<?php // phpcs:disable Generic.Commenting.Todo.TaskFound /** * OrdersTableQuery class file. */ namespace Automattic\WooCommerce\Internal\DataStores\Orders; use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil; defined( 'ABSPATH' ) || exit; /** * This class provides a `WP_Query`-like interface to custom order tables. * * @property-read int $found_orders Number of found orders. * @property-read int $found_posts Alias of the `$found_orders` property. * @property-read int $max_num_pages Max number of pages matching the current query. * @property-read array $orders Order objects, or order IDs. * @property-read array $posts Alias of the $orders property. */ class OrdersTableQuery { /** * Values to ignore when parsing query arguments. */ public const SKIPPED_VALUES = array( '', array(), null ); /** * Regex used to catch "shorthand" comparisons in date-related query args. */ public const REGEX_SHORTHAND_DATES = '/([^.<>]*)(>=|<=|>|<|\.\.\.)([^.<>]+)/'; /** * Highest possible unsigned bigint value (unsigned bigints being the type of the `id` column). * * This is deliberately held as a string, rather than a numeric type, for inclusion within queries. */ private const MYSQL_MAX_UNSIGNED_BIGINT = '18446744073709551615'; /** * Names of all COT tables (orders, addresses, operational_data, meta) in the form 'table_id' => 'table name'. * * @var array */ private $tables = array(); /** * Column mappings for all COT tables. * * @var array */ private $mappings = array(); /** * Query vars after processing and sanitization. * * @var array */ private $args = array(); /** * Columns to be selected in the SELECT clause. * * @var array */ private $fields = array(); /** * Array of table aliases and conditions used to compute the JOIN clause of the query. * * @var array */ private $join = array(); /** * Array of fields and conditions used to compute the WHERE clause of the query. * * @var array */ private $where = array(); /** * Field to be used in the GROUP BY clause of the query. * * @var array */ private $groupby = array(); /** * Array of fields used to compute the ORDER BY clause of the query. * * @var array */ private $orderby = array(); /** * Limits used to compute the LIMIT clause of the query. * * @var array */ private $limits = array(); /** * Results (order IDs) for the current query. * * @var array */ private $results = array(); /** * Final SQL query to run after processing of args. * * @var string */ private $sql = ''; /** * The number of pages (when pagination is enabled). * * @var int */ private $max_num_pages = 0; /** * The number of orders found. * * @var int */ private $found_orders = 0; /** * Meta query parser. * * @var OrdersTableMetaQuery */ private $meta_query = null; /** * Search query parser. * * @var OrdersTableSearchQuery? */ private $search_query = null; /** * Date query parser. * * @var WP_Date_Query */ private $date_query = null; /** * Sets up and runs the query after processing arguments. * * @param array $args Array of query vars. */ public function __construct( $args = array() ) { global $wpdb; $datastore = wc_get_container()->get( OrdersTableDataStore::class ); // TODO: maybe OrdersTableDataStore::get_all_table_names() could return these keys/indices instead. $this->tables = array( 'orders' => $datastore::get_orders_table_name(), 'addresses' => $datastore::get_addresses_table_name(), 'operational_data' => $datastore::get_operational_data_table_name(), 'meta' => $datastore::get_meta_table_name(), 'items' => $wpdb->prefix . 'woocommerce_order_items', ); $this->mappings = $datastore->get_all_order_column_mappings(); $this->args = $args; // TODO: args to be implemented. unset( $this->args['customer_note'], $this->args['name'] ); $this->build_query(); $this->run_query(); } /** * Remaps some legacy and `WP_Query` specific query vars to vars available in the customer order table scheme. * * @return void */ private function maybe_remap_args(): void { $mapping = array( // WP_Query legacy. 'post_date' => 'date_created_gmt', 'post_date_gmt' => 'date_created_gmt', 'post_modified' => 'date_modified_gmt', 'post_modified_gmt' => 'date_updated_gmt', 'post_status' => 'status', '_date_completed' => 'date_completed_gmt', '_date_paid' => 'date_paid_gmt', 'paged' => 'page', 'post_parent' => 'parent_order_id', 'post_parent__in' => 'parent_order_id', 'post_parent__not_in' => 'parent_exclude', 'post__not_in' => 'exclude', 'posts_per_page' => 'limit', 'p' => 'id', 'post__in' => 'id', 'post_type' => 'type', 'fields' => 'return', 'customer_user' => 'customer_id', 'order_currency' => 'currency', 'order_version' => 'woocommerce_version', 'cart_discount' => 'discount_total_amount', 'cart_discount_tax' => 'discount_tax_amount', 'order_shipping' => 'shipping_total_amount', 'order_shipping_tax' => 'shipping_tax_amount', 'order_tax' => 'tax_amount', // Translate from WC_Order_Query to table structure. 'version' => 'woocommerce_version', 'date_created' => 'date_created_gmt', 'date_modified' => 'date_updated_gmt', 'date_modified_gmt' => 'date_updated_gmt', 'date_completed' => 'date_completed_gmt', 'date_completed_gmt' => 'date_completed_gmt', 'date_paid' => 'date_paid_gmt', 'discount_total' => 'discount_total_amount', 'discount_tax' => 'discount_tax_amount', 'shipping_total' => 'shipping_total_amount', 'shipping_tax' => 'shipping_tax_amount', 'cart_tax' => 'tax_amount', 'total' => 'total_amount', 'customer_ip_address' => 'ip_address', 'customer_user_agent' => 'user_agent', 'parent' => 'parent_order_id', ); foreach ( $mapping as $query_key => $table_field ) { if ( isset( $this->args[ $query_key ] ) ) { $this->args[ $table_field ] = $this->args[ $query_key ]; unset( $this->args[ $query_key ] ); } } // meta_query. $this->args['meta_query'] = ( $this->arg_isset( 'meta_query' ) && is_array( $this->args['meta_query'] ) ) ? $this->args['meta_query'] : array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query $shortcut_meta_query = array(); foreach ( array( 'key', 'value', 'compare', 'type', 'compare_key', 'type_key' ) as $key ) { if ( $this->arg_isset( "meta_{$key}" ) ) { $shortcut_meta_query[ $key ] = $this->args[ "meta_{$key}" ]; } } if ( ! empty( $shortcut_meta_query ) ) { if ( ! empty( $this->args['meta_query'] ) ) { $this->args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'AND', $shortcut_meta_query, $this->args['meta_query'], ); } else { $this->args['meta_query'] = array( $shortcut_meta_query ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query } } } /** * Generates a `WP_Date_Query` compatible query from a given date. * 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. * @return array An array with keys 'year', 'month', 'day' and possibly 'hour', 'minute' and 'second'. */ 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( 'UTC' ) ); } elseif ( ! is_a( $date, 'WC_DateTime' ) ) { // YYYY-MM-DD queries have 'day' precision for backwards compat. $date = wc_string_to_datetime( $date ); $precision = 'day'; } $result['year'] = $date->date( 'Y' ); $result['month'] = $date->date( 'm' ); $result['day'] = $date->date( 'd' ); if ( 'second' === $precision ) { $result['hour'] = $date->date( 'H' ); $result['minute'] = $date->date( 'i' ); $result['second'] = $date->date( 's' ); } return $result; } /** * Processes date-related query args and merges the result into 'date_query'. * * @return void * @throws \Exception When date args are invalid. */ private function process_date_args(): void { $valid_operators = array( '>', '>=', '=', '<=', '<', '...' ); $date_queries = array(); $gmt_date_keys = array( 'date_created_gmt', 'date_updated_gmt', 'date_paid_gmt', 'date_completed_gmt', ); foreach ( array_filter( $gmt_date_keys, array( $this, 'arg_isset' ) ) as $date_key ) { $date_value = $this->args[ $date_key ]; $operator = '='; $dates = array(); 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] ); } $dates[] = $this->date_to_date_query_arg( $matches[3] ); } else { $dates[] = $this->date_to_date_query_arg( $date_value ); } if ( empty( $dates ) || ! $operator || ( '...' === $operator && count( $dates ) < 2 ) ) { throw new \Exception( 'Invalid date_query' ); } $operator_to_keys = array(); if ( in_array( $operator, array( '>', '>=', '...' ), true ) ) { $operator_to_keys[] = 'after'; } if ( in_array( $operator, array( '<', '<=', '...' ), true ) ) { $operator_to_keys[] = 'before'; } $date_queries[] = array_merge( array( 'column' => $date_key, 'inclusive' => ! in_array( $operator, array( '<', '>' ), true ), ), '=' === $operator ? end( $dates ) : array_combine( $operator_to_keys, $dates ) ); } // Add top-level date parameters to the date_query. $tl_query = array(); foreach ( array( 'hour', 'minute', 'second', 'year', 'monthnum', 'week', 'day', 'year' ) as $tl_key ) { if ( $this->arg_isset( $tl_key ) ) { $tl_query[ $tl_key ] = $this->args[ $tl_key ]; unset( $this->args[ $tl_key ] ); } } if ( $tl_query ) { $tl_query['column'] = 'date_created_gmt'; $date_queries[] = $tl_query; } if ( $date_queries ) { if ( ! $this->arg_isset( 'date_query' ) ) { $this->args['date_query'] = array(); } $this->args['date_query'] = array_merge( array( 'relation' => 'AND' ), $date_queries, $this->args['date_query'] ); } $this->process_date_query_columns(); } /** * Makes sure all 'date_query' columns are correctly prefixed and their respective tables are being JOIN'ed. * * @return void */ private function process_date_query_columns() { global $wpdb; $legacy_columns = array( 'post_date' => 'date_created_gmt', 'post_date_gmt' => 'date_created_gmt', 'post_modified' => 'date_modified_gmt', 'post_modified_gmt' => 'date_updated_gmt', ); $table_mapping = array( 'date_created_gmt' => $this->tables['orders'], 'date_updated_gmt' => $this->tables['orders'], 'date_paid_gmt' => $this->tables['operational_data'], 'date_completed_gmt' => $this->tables['operational_data'], ); if ( empty( $this->args['date_query'] ) ) { return; } array_walk_recursive( $this->args['date_query'], function( &$value, $key ) use ( $legacy_columns, $table_mapping, $wpdb ) { if ( 'column' !== $key ) { return; } // Translate legacy columns from wp_posts if necessary. $value = ( isset( $legacy_columns[ $value ] ) || isset( $legacy_columns[ "{$wpdb->posts}.{$value}" ] ) ) ? $legacy_columns[ $value ] : $value; $table = $table_mapping[ $value ] ?? null; if ( ! $table ) { return; } $value = "{$table}.{$value}"; if ( $table !== $this->tables['orders'] ) { $this->join( $table, '', '', 'inner', true ); } } ); } /** * Sanitizes the 'status' query var. * * @return void */ private function sanitize_status(): void { // Sanitize status. $valid_statuses = array_keys( wc_get_order_statuses() ); if ( empty( $this->args['status'] ) || 'any' === $this->args['status'] ) { $this->args['status'] = $valid_statuses; } elseif ( 'all' === $this->args['status'] ) { $this->args['status'] = array(); } else { $this->args['status'] = is_array( $this->args['status'] ) ? $this->args['status'] : array( $this->args['status'] ); foreach ( $this->args['status'] as &$status ) { $status = in_array( 'wc-' . $status, $valid_statuses, true ) ? 'wc-' . $status : $status; } $this->args['status'] = array_unique( array_filter( $this->args['status'] ) ); } } /** * Parses and sanitizes the 'orderby' query var. * * @return void */ private function sanitize_order_orderby(): void { // Allowed keys. // TODO: rand, meta keys, etc. $allowed_keys = array( 'ID', 'id', 'type', 'date', 'modified', 'parent' ); // Translate $orderby to a valid field. $mapping = array( 'ID' => "{$this->tables['orders']}.id", 'id' => "{$this->tables['orders']}.id", 'type' => "{$this->tables['orders']}.type", 'date' => "{$this->tables['orders']}.date_created_gmt", 'date_created' => "{$this->tables['orders']}.date_created_gmt", 'modified' => "{$this->tables['orders']}.date_updated_gmt", 'date_modified' => "{$this->tables['orders']}.date_updated_gmt", 'parent' => "{$this->tables['orders']}.parent_order_id", 'total' => "{$this->tables['orders']}.total_amount", 'order_total' => "{$this->tables['orders']}.total_amount", ); $order = $this->args['order'] ?? ''; $orderby = $this->args['orderby'] ?? ''; if ( 'none' === $orderby ) { return; } if ( is_string( $orderby ) ) { $orderby = array( $orderby => $order ); } $this->args['orderby'] = array(); foreach ( $orderby as $order_key => $order ) { if ( isset( $mapping[ $order_key ] ) ) { $this->args['orderby'][ $mapping[ $order_key ] ] = $this->sanitize_order( $order ); } } } /** * Makes sure the order in an ORDER BY statement is either 'ASC' o 'DESC'. * * @param string $order The unsanitized order. * @return string The sanitized order. */ private function sanitize_order( string $order ): string { $order = strtoupper( $order ); return in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'DESC'; } /** * Builds the final SQL query to be run. * * @return void */ private function build_query(): void { $this->maybe_remap_args(); // Build query. $this->process_date_args(); $this->process_orders_table_query_args(); $this->process_operational_data_table_query_args(); $this->process_addresses_table_query_args(); // Search queries. if ( ! empty( $this->args['s'] ) ) { $this->search_query = new OrdersTableSearchQuery( $this ); $sql = $this->search_query->get_sql_clauses(); $this->join = $sql['join'] ? array_merge( $this->join, $sql['join'] ) : $this->join; $this->where = $sql['where'] ? array_merge( $this->where, $sql['where'] ) : $this->where; } // Meta queries. if ( ! empty( $this->args['meta_query'] ) ) { $this->meta_query = new OrdersTableMetaQuery( $this ); $sql = $this->meta_query->get_sql_clauses(); $this->join = $sql['join'] ? array_merge( $this->join, $sql['join'] ) : $this->join; $this->where = $sql['where'] ? array_merge( $this->where, array( $sql['where'] ) ) : $this->where; if ( $sql['join'] ) { $this->groupby[] = "{$this->tables['orders']}.id"; } } // Date queries. if ( ! empty( $this->args['date_query'] ) ) { $this->date_query = new \WP_Date_Query( $this->args['date_query'], "{$this->tables['orders']}.date_created_gmt" ); $this->where[] = substr( trim( $this->date_query->get_sql() ), 3 ); // WP_Date_Query includes "AND". } $this->process_orderby(); $this->process_limit(); $orders_table = $this->tables['orders']; // SELECT [fields]. $this->fields = "{$orders_table}.id"; $fields = $this->fields; // SQL_CALC_FOUND_ROWS. if ( ( ! $this->arg_isset( 'no_found_rows' ) || ! $this->args['no_found_rows'] ) && $this->limits ) { $found_rows = 'SQL_CALC_FOUND_ROWS'; } else { $found_rows = ''; } // JOIN. $join = implode( ' ', $this->join ); // WHERE. $where = '1=1'; foreach ( $this->where as $_where ) { $where .= " AND ({$_where})"; } // ORDER BY. $orderby = $this->orderby ? ( 'ORDER BY ' . implode( ', ', $this->orderby ) ) : ''; // LIMITS. $limits = ''; if ( ! empty( $this->limits ) && count( $this->limits ) === 2 ) { list( $offset, $row_count ) = $this->limits; $row_count = $row_count === -1 ? self::MYSQL_MAX_UNSIGNED_BIGINT : (int) $row_count; $limits = 'LIMIT ' . (int) $offset . ', ' . $row_count; } // GROUP BY. $groupby = $this->groupby ? 'GROUP BY ' . implode( ', ', (array) $this->groupby ) : ''; $this->sql = "SELECT $found_rows DISTINCT $fields FROM $orders_table $join WHERE $where $groupby $orderby $limits"; } /** * JOINs the main orders table with another table. * * @param string $table Table name (including prefix). * @param string $alias Table alias to use. Defaults to $table. * @param string $on ON clause. Defaults to "wc_orders.id = {$alias}.order_id". * @param string $join_type JOIN type: LEFT, RIGHT or INNER. * @param boolean $alias_once If TRUE, table won't be JOIN'ed again if already JOIN'ed. * @return void * @throws \Exception When an error occurs, such as trying to re-use an alias with $alias_once = FALSE. */ private function join( string $table, string $alias = '', string $on = '', string $join_type = 'inner', bool $alias_once = false ) { $alias = empty( $alias ) ? $table : $alias; $join_type = strtoupper( trim( $join_type ) ); if ( $this->tables['orders'] === $alias ) { // translators: %s is a table name. throw new \Exception( sprintf( __( '%s can not be used as a table alias in OrdersTableQuery', 'woocommerce' ), $alias ) ); } if ( empty( $on ) ) { if ( $this->tables['orders'] === $table ) { $on = "{$this->tables['orders']}.id = {$alias}.id"; } else { $on = "{$this->tables['orders']}.id = {$alias}.order_id"; } } if ( isset( $this->join[ $alias ] ) ) { if ( ! $alias_once ) { // translators: %s is a table name. throw new \Exception( sprintf( __( 'Can not re-use table alias "%s" in OrdersTableQuery.', 'woocommerce' ), $alias ) ); } return; } if ( '' === $join_type || ! in_array( $join_type, array( 'LEFT', 'RIGHT', 'INNER' ), true ) ) { $join_type = 'INNER'; } $sql_join = ''; $sql_join .= "{$join_type} JOIN {$table} "; $sql_join .= ( $alias !== $table ) ? "AS {$alias} " : ''; $sql_join .= "ON ( {$on} )"; $this->join[ $alias ] = $sql_join; } /** * Generates a properly escaped and sanitized WHERE condition for a given field. * * @param string $table The table the field belongs to. * @param string $field The field or column name. * @param string $operator The operator to use in the condition. Defaults to '=' or 'IN' depending on $value. * @param mixed $value The value. * @param string $type The column type as specified in {@see OrdersTableDataStore} column mappings. * @return string The resulting WHERE condition. */ public function where( string $table, string $field, string $operator, $value, string $type ): string { global $wpdb; $db_util = wc_get_container()->get( DatabaseUtil::class ); $operator = strtoupper( '' !== $operator ? $operator : '=' ); try { $format = $db_util->get_wpdb_format_for_type( $type ); } catch ( \Exception $e ) { $format = '%s'; } // = and != can be shorthands for IN and NOT in for array values. if ( is_array( $value ) && '=' === $operator ) { $operator = 'IN'; } elseif ( is_array( $value ) && '!=' === $operator ) { $operator = 'NOT IN'; } if ( ! in_array( $operator, array( '=', '!=', 'IN', 'NOT IN' ), true ) ) { return false; } if ( is_array( $value ) ) { $value = array_map( array( $db_util, 'format_object_value_for_db' ), $value, array_fill( 0, count( $value ), $type ) ); } else { $value = $db_util->format_object_value_for_db( $value, $type ); } if ( is_array( $value ) ) { $placeholder = array_fill( 0, count( $value ), $format ); $placeholder = '(' . implode( ',', $placeholder ) . ')'; } else { $placeholder = $format; } $sql = $wpdb->prepare( "{$table}.{$field} {$operator} {$placeholder}", $value ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare return $sql; } /** * Processes fields related to the orders table. * * @return void */ private function process_orders_table_query_args(): void { $this->sanitize_status(); $fields = array_filter( array( 'id', 'status', 'type', 'currency', 'tax_amount', 'customer_id', 'billing_email', 'total_amount', 'parent_order_id', 'payment_method', 'payment_method_title', 'transaction_id', 'ip_address', 'user_agent', ), array( $this, 'arg_isset' ) ); foreach ( $fields as $arg_key ) { $this->where[] = $this->where( $this->tables['orders'], $arg_key, '=', $this->args[ $arg_key ], $this->mappings['orders'][ $arg_key ]['type'] ); } if ( $this->arg_isset( 'parent_exclude' ) ) { $this->where[] = $this->where( $this->tables['orders'], 'parent_order_id', '!=', $this->args['parent_exclude'], 'int' ); } if ( $this->arg_isset( 'exclude' ) ) { $this->where[] = $this->where( $this->tables['orders'], 'id', '!=', $this->args['exclude'], 'int' ); } // 'customer' is a very special field. if ( $this->arg_isset( 'customer' ) ) { $customer_query = $this->generate_customer_query( $this->args['customer'] ); if ( $customer_query ) { $this->where[] = $customer_query; } } } /** * Generate SQL conditions for the 'customer' query. * * @param array $values List of customer ids or emails. * @param string $relation 'OR' or 'AND' relation used to build the customer query. * @return string SQL to be used in a WHERE clause. */ private function generate_customer_query( $values, string $relation = 'OR' ): string { $values = is_array( $values ) ? $values : array( $values ); $ids = array(); $emails = array(); foreach ( $values as $value ) { if ( is_array( $value ) ) { $sql = $this->generate_customer_query( $value, 'AND' ); $pieces[] = $sql ? '(' . $sql . ')' : ''; } elseif ( is_numeric( $value ) ) { $ids[] = absint( $value ); } elseif ( is_string( $value ) && is_email( $value ) ) { $emails[] = sanitize_email( $value ); } } if ( $ids ) { $pieces[] = $this->where( $this->tables['orders'], 'customer_id', '=', $ids, 'int' ); } if ( $emails ) { $pieces[] = $this->where( $this->tables['orders'], 'billing_email', '=', $emails, 'string' ); } return $pieces ? implode( " $relation ", $pieces ) : ''; } /** * Processes fields related to the operational data table. * * @return void */ private function process_operational_data_table_query_args(): void { $fields = array_filter( array( 'created_via', 'woocommerce_version', 'prices_include_tax', 'order_key', 'discount_total_amount', 'discount_tax_amount', 'shipping_total_amount', 'shipping_tax_amount', ), array( $this, 'arg_isset' ) ); if ( ! $fields ) { return; } $this->join( $this->tables['operational_data'], '', '', 'inner', true ); foreach ( $fields as $arg_key ) { $this->where[] = $this->where( $this->tables['operational_data'], $arg_key, '=', $this->args[ $arg_key ], $this->mappings['operational_data'][ $arg_key ]['type'] ); } } /** * Processes fields related to the addresses table. * * @return void */ private function process_addresses_table_query_args(): void { global $wpdb; foreach ( array( 'billing', 'shipping' ) as $address_type ) { $fields = array_filter( array( $address_type . '_first_name', $address_type . '_last_name', $address_type . '_company', $address_type . '_address_1', $address_type . '_address_2', $address_type . '_city', $address_type . '_state', $address_type . '_postcode', $address_type . '_country', $address_type . '_phone', ), array( $this, 'arg_isset' ) ); if ( ! $fields ) { continue; } $this->join( $this->tables['addresses'], $address_type, $wpdb->prepare( "{$this->tables['orders']}.id = {$address_type}.order_id AND {$address_type}.address_type = %s", $address_type ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 'inner', false ); foreach ( $fields as $arg_key ) { $column_name = str_replace( "{$address_type}_", '', $arg_key ); $this->where[] = $this->where( $address_type, $column_name, '=', $this->args[ $arg_key ], $this->mappings[ "{$address_type}_address" ][ $column_name ]['type'] ); } } } /** * Generates the ORDER BY clause. * * @return void */ private function process_orderby(): void { // 'order' and 'orderby' vars. $this->args['order'] = $this->sanitize_order( $this->args['order'] ?? '' ); $this->sanitize_order_orderby(); $orderby = $this->args['orderby']; if ( 'none' === $orderby ) { $this->orderby = ''; return; } $orderby_array = array(); foreach ( $this->args['orderby'] as $_orderby => $order ) { $orderby_array[] = "{$_orderby} {$order}"; } $this->orderby = $orderby_array; } /** * Generates the limits to be used in the LIMIT clause. * * @return void */ private function process_limit(): void { $row_count = ( $this->arg_isset( 'limit' ) ? (int) $this->args['limit'] : false ); $page = ( $this->arg_isset( 'page' ) ? absint( $this->args['page'] ) : 1 ); $offset = ( $this->arg_isset( 'offset' ) ? absint( $this->args['offset'] ) : false ); // Bool false indicates no limit was specified; less than -1 means an invalid value was passed (such as -3). if ( $row_count === false || $row_count < -1 ) { return; } if ( $offset === false && $row_count > -1 ) { $offset = (int) ( ( $page - 1 ) * $row_count ); } $this->limits = array( $offset, $row_count ); } /** * Checks if a query var is set (i.e. not one of the "skipped values"). * * @param string $arg_key Query var. * @return bool TRUE if query var is set. */ public function arg_isset( string $arg_key ): bool { return ( isset( $this->args[ $arg_key ] ) && ! in_array( $this->args[ $arg_key ], self::SKIPPED_VALUES, true ) ); } /** * Runs the SQL query. * * @return void */ private function run_query(): void { global $wpdb; // Run query. $this->orders = array_map( 'absint', $wpdb->get_col( $this->sql ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared // Set max_num_pages and found_orders if necessary. if ( ( $this->arg_isset( 'no_found_rows' ) && ! $this->args['no_found_rows'] ) || empty( $this->orders ) ) { return; } if ( $this->limits ) { $this->found_orders = absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) ); $this->max_num_pages = (int) ceil( $this->found_orders / $this->args['limit'] ); } else { $this->found_orders = count( $this->orders ); } } /** * Make some private available for backwards compatibility. * * @param string $name Property to get. * @return mixed */ public function __get( string $name ) { switch ( $name ) { case 'found_orders': case 'found_posts': return $this->found_orders; case 'max_num_pages': return $this->max_num_pages; case 'posts': case 'orders': return $this->results; case 'request': return $this->sql; default: break; } } /** * Returns the value of one of the query arguments. * * @param string $arg_name Query var. * @return mixed */ public function get( string $arg_name ) { return $this->args[ $arg_name ] ?? null; } /** * Returns the name of one of the OrdersTableDatastore tables. * * @param string $table_id Table identifier. One of 'orders', 'operational_data', 'addresses', 'meta'. * @return string The prefixed table name. * @throws \Exception When table ID is not found. */ public function get_table_name( string $table_id = '' ): string { if ( ! isset( $this->tables[ $table_id ] ) ) { // Translators: %s is a table identifier. throw new \Exception( sprintf( __( 'Invalid table id: %s.', 'woocommerce' ), $table_id ) ); } return $this->tables[ $table_id ]; } }