diff --git a/includes/API/Product_Categories_Controller.php b/includes/API/Product_Categories_Controller.php index 77da294..954bb06 100644 --- a/includes/API/Product_Categories_Controller.php +++ b/includes/API/Product_Categories_Controller.php @@ -2,9 +2,9 @@ namespace WCPOS\WooCommercePOS\API; -\defined('ABSPATH') || die; +\defined( 'ABSPATH' ) || die; -if ( ! class_exists('WC_REST_Product_Categories_Controller') ) { +if ( ! class_exists( 'WC_REST_Product_Categories_Controller' ) ) { return; } @@ -30,6 +30,13 @@ class Product_Categories_Controller extends WC_REST_Product_Categories_Controlle */ protected $namespace = 'wcpos/v1'; + /** + * Store the request object for use in lifecycle methods. + * + * @var WP_REST_Request + */ + protected $wcpos_request; + /** * Constructor. */ @@ -50,6 +57,7 @@ public function __construct() { * @param array $handler Route handler used for the request. */ public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $request, $route, $handler ) { + $this->wcpos_request = $request; $this->wcpos_register_wc_rest_api_hooks(); $params = $request->get_params(); @@ -66,6 +74,7 @@ public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $reque */ public function wcpos_register_wc_rest_api_hooks(): void { add_filter( 'woocommerce_rest_prepare_product_cat', array( $this, 'wcpos_product_categories_response' ), 10, 3 ); + add_filter( 'woocommerce_rest_product_cat_query', array( $this, 'wcpos_product_category_query' ), 10, 2 ); } /** @@ -89,6 +98,61 @@ public function wcpos_product_categories_response( WP_REST_Response $response, o return $response; } + /** + * Filter the tag query. + * + * @param array $args Query arguments. + * @param WP_REST_Request $request Request object. + */ + public function wcpos_product_category_query( array $args, WP_REST_Request $request ): array { + // Check for wcpos_include/wcpos_exclude parameter. + if ( isset( $request['wcpos_include'] ) || isset( $request['wcpos_exclude'] ) ) { + // Add a custom WHERE clause to the query. + add_filter( 'terms_clauses', array( $this, 'wcpos_terms_clauses_include_exclude' ), 10, 3 ); + } + + return $args; + } + + /** + * Filters the terms query SQL clauses. + * + * @param string[] $clauses { + * Associative array of the clauses for the query. + * + * @type string $fields The SELECT clause of the query. + * @type string $join The JOIN clause of the query. + * @type string $where The WHERE clause of the query. + * @type string $distinct The DISTINCT clause of the query. + * @type string $orderby The ORDER BY clause of the query. + * @type string $order The ORDER clause of the query. + * @type string $limits The LIMIT clause of the query. + * } + * @param string[] $taxonomies An array of taxonomy names. + * @param array $args An array of term query arguments. + * + * @return string[] $clauses + */ + public function wcpos_terms_clauses_include_exclude( array $clauses, array $taxonomies, array $args ) { + global $wpdb; + + // Handle 'wcpos_include' + if ( ! empty( $this->wcpos_request['wcpos_include'] ) ) { + $include_ids = array_map( 'intval', $this->wcpos_request['wcpos_include'] ); + $ids_format = implode( ',', array_fill( 0, count( $include_ids ), '%d' ) ); + $clauses['where'] .= $wpdb->prepare( " AND t.term_id IN ($ids_format) ", $include_ids ); + } + + // Handle 'wcpos_exclude' + if ( ! empty( $this->wcpos_request['wcpos_exclude'] ) ) { + $exclude_ids = array_map( 'intval', $this->wcpos_request['wcpos_exclude'] ); + $ids_format = implode( ',', array_fill( 0, count( $exclude_ids ), '%d' ) ); + $clauses['where'] .= $wpdb->prepare( " AND t.term_id NOT IN ($ids_format) ", $exclude_ids ); + } + + return $clauses; + } + /** * Returns array of all product category ids. * diff --git a/includes/API/Product_Tags_Controller.php b/includes/API/Product_Tags_Controller.php index 291bfde..73dbf92 100644 --- a/includes/API/Product_Tags_Controller.php +++ b/includes/API/Product_Tags_Controller.php @@ -2,9 +2,9 @@ namespace WCPOS\WooCommercePOS\API; -\defined('ABSPATH') || die; +\defined( 'ABSPATH' ) || die; -if ( ! class_exists('WC_REST_Product_Tags_Controller') ) { +if ( ! class_exists( 'WC_REST_Product_Tags_Controller' ) ) { return; } @@ -30,6 +30,13 @@ class Product_Tags_Controller extends WC_REST_Product_Tags_Controller { */ protected $namespace = 'wcpos/v1'; + /** + * Store the request object for use in lifecycle methods. + * + * @var WP_REST_Request + */ + protected $wcpos_request; + /** * Constructor. */ @@ -50,6 +57,7 @@ public function __construct() { * @param array $handler Route handler used for the request. */ public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $request, $route, $handler ) { + $this->wcpos_request = $request; $this->wcpos_register_wc_rest_api_hooks(); $params = $request->get_params(); @@ -66,6 +74,7 @@ public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $reque */ public function wcpos_register_wc_rest_api_hooks(): void { add_filter( 'woocommerce_rest_prepare_product_tag', array( $this, 'wcpos_product_tags_response' ), 10, 3 ); + add_filter( 'woocommerce_rest_product_tag_query', array( $this, 'wcpos_product_tag_query' ), 10, 2 ); } /** @@ -89,6 +98,61 @@ public function wcpos_product_tags_response( WP_REST_Response $response, object return $response; } + /** + * Filter the tag query. + * + * @param array $args Query arguments. + * @param WP_REST_Request $request Request object. + */ + public function wcpos_product_tag_query( array $args, WP_REST_Request $request ): array { + // Check for wcpos_include/wcpos_exclude parameter. + if ( isset( $request['wcpos_include'] ) || isset( $request['wcpos_exclude'] ) ) { + // Add a custom WHERE clause to the query. + add_filter( 'terms_clauses', array( $this, 'wcpos_terms_clauses_include_exclude' ), 10, 3 ); + } + + return $args; + } + + /** + * Filters the terms query SQL clauses. + * + * @param string[] $clauses { + * Associative array of the clauses for the query. + * + * @type string $fields The SELECT clause of the query. + * @type string $join The JOIN clause of the query. + * @type string $where The WHERE clause of the query. + * @type string $distinct The DISTINCT clause of the query. + * @type string $orderby The ORDER BY clause of the query. + * @type string $order The ORDER clause of the query. + * @type string $limits The LIMIT clause of the query. + * } + * @param string[] $taxonomies An array of taxonomy names. + * @param array $args An array of term query arguments. + * + * @return string[] $clauses + */ + public function wcpos_terms_clauses_include_exclude( array $clauses, array $taxonomies, array $args ) { + global $wpdb; + + // Handle 'wcpos_include' + if ( ! empty( $this->wcpos_request['wcpos_include'] ) ) { + $include_ids = array_map( 'intval', $this->wcpos_request['wcpos_include'] ); + $ids_format = implode( ',', array_fill( 0, count( $include_ids ), '%d' ) ); + $clauses['where'] .= $wpdb->prepare( " AND t.term_id IN ($ids_format) ", $include_ids ); + } + + // Handle 'wcpos_exclude' + if ( ! empty( $this->wcpos_request['wcpos_exclude'] ) ) { + $exclude_ids = array_map( 'intval', $this->wcpos_request['wcpos_exclude'] ); + $ids_format = implode( ',', array_fill( 0, count( $exclude_ids ), '%d' ) ); + $clauses['where'] .= $wpdb->prepare( " AND t.term_id NOT IN ($ids_format) ", $exclude_ids ); + } + + return $clauses; + } + /** * Returns array of all product tag ids. * diff --git a/includes/API/Product_Variations_Controller.php b/includes/API/Product_Variations_Controller.php index 43cc3c4..92ab04e 100644 --- a/includes/API/Product_Variations_Controller.php +++ b/includes/API/Product_Variations_Controller.php @@ -152,6 +152,7 @@ public function wcpos_register_wc_rest_api_hooks(): void { add_filter( 'wp_get_attachment_image_src', array( $this, 'wcpos_product_image_src' ), 10, 4 ); add_action( 'woocommerce_rest_insert_product_variation_object', array( $this, 'wcpos_insert_product_variation_object' ), 10, 3 ); add_filter( 'woocommerce_rest_product_variation_object_query', array( $this, 'wcpos_product_variation_query' ), 10, 2 ); + add_filter( 'posts_search', array( $this, 'wcpos_posts_search' ), 10, 2 ); } /** @@ -209,6 +210,106 @@ public function wcpos_insert_product_variation_object( WC_Data $object, WP_REST_ } } + /** + * Filter to adjust the WordPress search SQL query + * - Search for the variation SKU and barcode + * - Do not search variation description. + * + * @param string $search Search string. + * @param WP_Query $wp_query WP_Query object. + * + * @return string + */ + public function wcpos_posts_search( string $search, WP_Query $wp_query ) { + global $wpdb; + + if ( empty( $search ) ) { + return $search; // skip processing - no search term in query. + } + + $q = $wp_query->query_vars; + $n = ! empty( $q['exact'] ) ? '' : '%'; + $search_terms = (array) $q['search_terms']; + + // Fields in the main 'posts' table. + $post_fields = array(); // nothing at the moment for variations. + + // Meta fields to search. + $meta_fields = array( '_sku' ); + $barcode_field = $this->wcpos_get_barcode_field(); + if ( '_sku' !== $barcode_field ) { + $meta_fields[] = $barcode_field; + } + + $barcode_field = $this->wcpos_get_barcode_field(); + if ( '_sku' !== $barcode_field ) { + $fields_to_search[] = $barcode_field; + } + + $search_conditions = array(); + + foreach ( $search_terms as $term ) { + $term = $n . $wpdb->esc_like( $term ) . $n; + + // Search in post fields + foreach ( $post_fields as $field ) { + if ( ! empty( $field ) ) { + $search_conditions[] = $wpdb->prepare( "($wpdb->posts.$field LIKE %s)", $term ); + } + } + + // Search in meta fields + foreach ( $meta_fields as $field ) { + $search_conditions[] = $wpdb->prepare( '(pm1.meta_value LIKE %s AND pm1.meta_key = %s)', $term, $field ); + } + } + + if ( ! empty( $search_conditions ) ) { + $search = ' AND (' . implode( ' OR ', $search_conditions ) . ') '; + if ( ! is_user_logged_in() ) { + $search .= " AND ($wpdb->posts.post_password = '') "; + } + } + + return $search; + } + + /** + * Filters the JOIN clause of the query. + * + * @param string $join The JOIN clause of the query. + * @param WP_Query $query The WP_Query instance (passed by reference). + * + * @return string + */ + public function wcpos_posts_join_to_posts_search( string $join, WP_Query $query ) { + global $wpdb; + + if ( ! empty( $query->query_vars['s'] ) && false === strpos( $join, 'pm1' ) ) { + $join .= " LEFT JOIN {$wpdb->postmeta} pm1 ON {$wpdb->posts}.ID = pm1.post_id "; + } + + return $join; + } + + /** + * Filters the GROUP BY clause of the query. + * + * @param string $groupby The GROUP BY clause of the query. + * @param WP_Query $query The WP_Query instance (passed by reference). + * + * @return string + */ + public function wcpos_posts_groupby_posts_search( string $groupby, WP_Query $query ) { + global $wpdb; + + if ( ! empty( $query->query_vars['s'] ) ) { + $groupby = "{$wpdb->posts}.ID"; + } + + return $groupby; + } + /** * Filter the query arguments for a request. * @@ -218,6 +319,12 @@ public function wcpos_insert_product_variation_object( WC_Data $object, WP_REST_ * @return array $args Key value array of query var to query value. */ public function wcpos_product_variation_query( array $args, WP_REST_Request $request ) { + if ( ! empty( $request['search'] ) ) { + // We need to set the query up for a postmeta join. + add_filter( 'posts_join', array( $this, 'wcpos_posts_join_to_posts_search' ), 10, 2 ); + add_filter( 'posts_groupby', array( $this, 'wcpos_posts_groupby_posts_search' ), 10, 2 ); + } + // Check for wcpos_include/wcpos_exclude parameter. if ( isset( $request['wcpos_include'] ) || isset( $request['wcpos_exclude'] ) ) { // Add a custom WHERE clause to the query. @@ -371,77 +478,6 @@ protected function prepare_objects_query( $request ) { * @param WP_REST_Request $request Full details about the request. */ public function wcpos_get_all_items( $request ) { - $query_args = $this->prepare_objects_query( $request ); - if ( is_wp_error( current( $query_args ) ) ) { - return current( $query_args ); - } - - // Check if 'search' param is set for SKU search - if ( ! empty( $query_args['s'] ) ) { - $barcode_field = $this->wcpos_get_barcode_field(); - $meta_query = array( - 'key' => '_sku', - 'value' => $query_args['s'], - 'compare' => 'LIKE', - ); - - if ( $barcode_field && '_sku' !== $barcode_field ) { - $meta_query = array( - 'relation' => 'OR', - $meta_query, - array( - 'key' => $barcode_field, - 'value' => $query_args['s'], - 'compare' => 'LIKE', - ), - ); - } - - // Combine meta queries - $query_args['meta_query'] = $this->wcpos_combine_meta_queries( $query_args['meta_query'], $meta_query ); - - unset( $query_args['s'] ); - } - - $query_results = $this->get_objects( $query_args ); - - $objects = array(); - foreach ( $query_results['objects'] as $object ) { - if ( ! \wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { - continue; - } - - $data = $this->prepare_object_for_response( $object, $request ); - $objects[] = $this->prepare_response_for_collection( $data ); - } - - $page = (int) $query_args['paged']; - $max_pages = $query_results['pages']; - - $response = rest_ensure_response( $objects ); - $response->header( 'X-WP-Total', $query_results['total'] ); - $response->header( 'X-WP-TotalPages', (int) $max_pages ); - - /** - * Note: custom endpoint for getting all product variations - */ - $base = 'products/variations'; - $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ) ); - - if ( $page > 1 ) { - $prev_page = $page - 1; - if ( $prev_page > $max_pages ) { - $prev_page = $max_pages; - } - $prev_link = add_query_arg( 'page', $prev_page, $base ); - $response->link_header( 'prev', $prev_link ); - } - if ( $max_pages > $page ) { - $next_page = $page + 1; - $next_link = add_query_arg( 'page', $next_page, $base ); - $response->link_header( 'next', $next_link ); - } - - return $response; + return parent::get_items( $request ); } } diff --git a/includes/API/Products_Controller.php b/includes/API/Products_Controller.php index 0c197e3..570b6b4 100644 --- a/includes/API/Products_Controller.php +++ b/includes/API/Products_Controller.php @@ -240,40 +240,48 @@ public function wcpos_posts_search( string $search, WP_Query $wp_query ) { global $wpdb; if ( empty( $search ) ) { - return $search; // skip processing - no search term in query + return $search; // skip processing - no search term in query. } $q = $wp_query->query_vars; $n = ! empty( $q['exact'] ) ? '' : '%'; + $search_terms = (array) $q['search_terms']; - $search = $searchand = ''; + // Fields in the main 'posts' table. + $post_fields = array( 'post_title' ); - // Adjust this to include the fields you want to search - $fields_to_search = array( - 'post_title', - '_sku', - ); + // Meta fields to search. + $meta_fields = array( '_sku' ); + $barcode_field = $this->wcpos_get_barcode_field(); + if ( '_sku' !== $barcode_field ) { + $meta_fields[] = $barcode_field; + } $barcode_field = $this->wcpos_get_barcode_field(); if ( '_sku' !== $barcode_field ) { $fields_to_search[] = $barcode_field; } - foreach ( (array) $q['search_terms'] as $term ) { + $search_conditions = array(); + + foreach ( $search_terms as $term ) { $term = $n . $wpdb->esc_like( $term ) . $n; - foreach ( $fields_to_search as $field ) { - if ( \in_array( $field, array( 'post_title' ), true ) ) { - $search .= $wpdb->prepare( "{$searchand}($wpdb->posts.$field LIKE %s)", $term ); - } else { - $search .= $wpdb->prepare( "{$searchand}(pm1.meta_value LIKE %s AND pm1.meta_key = '$field')", $term ); + // Search in post fields + foreach ( $post_fields as $field ) { + if ( ! empty( $field ) ) { + $search_conditions[] = $wpdb->prepare( "($wpdb->posts.$field LIKE %s)", $term ); } - $searchand = ' OR '; + } + + // Search in meta fields + foreach ( $meta_fields as $field ) { + $search_conditions[] = $wpdb->prepare( '(pm1.meta_value LIKE %s AND pm1.meta_key = %s)', $term, $field ); } } - if ( ! empty( $search ) ) { - $search = " AND ({$search}) "; + if ( ! empty( $search_conditions ) ) { + $search = ' AND (' . implode( ' OR ', $search_conditions ) . ') '; if ( ! is_user_logged_in() ) { $search .= " AND ($wpdb->posts.post_password = '') "; } diff --git a/includes/API/Taxes_Controller.php b/includes/API/Taxes_Controller.php index df1b081..5a13ac4 100644 --- a/includes/API/Taxes_Controller.php +++ b/includes/API/Taxes_Controller.php @@ -2,9 +2,9 @@ namespace WCPOS\WooCommercePOS\API; -\defined('ABSPATH') || die; +\defined( 'ABSPATH' ) || die; -if ( ! class_exists('WC_REST_Taxes_Controller') ) { +if ( ! class_exists( 'WC_REST_Taxes_Controller' ) ) { return; } @@ -27,6 +27,13 @@ class Taxes_Controller extends WC_REST_Taxes_Controller { */ protected $namespace = 'wcpos/v1'; + /** + * Store the request object for use in lifecycle methods. + * + * @var WP_REST_Request + */ + protected $wcpos_request; + /** * Constructor. */ @@ -39,163 +46,120 @@ public function __construct() { } /** - * Override the get_items method to add support for 'include' parameter. - * This is a copy of parent::get_items in the V1 Controller. - * - * @param WP_REST_Request $request Full details about the request. + * Dispatch request to parent controller, or override if needed. * - * @return WP_Error|WP_REST_Response + * @param mixed $dispatch_result Dispatch result, will be used if not empty. + * @param WP_REST_Request $request Request used to generate the response. + * @param string $route Route matched for the request. + * @param array $handler Route handler used for the request. */ - public function get_items( $request ) { - global $wpdb; + public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $request, $route, $handler ) { + $this->wcpos_request = $request; + $this->wcpos_register_wc_rest_api_hooks(); + $params = $request->get_params(); - $prepared_args = array(); - $prepared_args['order'] = $request['order']; - $prepared_args['number'] = $request['per_page']; - if ( ! empty( $request['offset'] ) ) { - $prepared_args['offset'] = $request['offset']; - } else { - $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; - } - $orderby_possibles = array( - 'id' => 'tax_rate_id', - 'order' => 'tax_rate_order', - 'priority' => 'tax_rate_priority', - ); - $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; - $prepared_args['class'] = $request['class']; - - // Add support for 'include' parameter - $include_sql = ''; - if ( ! empty($request['include'])) { - $include_ids = \is_array($request['include']) ? $request['include'] : array($request['include']); - $include_ids = array_map('absint', $include_ids); // Sanitize the IDs - - $include_sql = sprintf(" AND (tax_rate_id IN (%s))", implode(',', $include_ids)); + // Optimised query for getting all product IDs + if ( isset( $params['posts_per_page'] ) && -1 == $params['posts_per_page'] && isset( $params['fields'] ) ) { + $dispatch_result = $this->wcpos_get_all_posts( $params['fields'] ); } - /** - * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API. - * - * @param array $prepared_args Array of arguments for $wpdb->get_results(). - * @param WP_REST_Request $request The current request. - */ - $prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request ); - - $orderby = sanitize_key( $prepared_args['orderby'] ) . ' ' . sanitize_key( $prepared_args['order'] ); - // Modify the main query to include the 'include' condition - $query = " - SELECT * - FROM {$wpdb->prefix}woocommerce_tax_rates - WHERE 1=1 - {$include_sql} - %s - ORDER BY {$orderby} - LIMIT %%d, %%d - "; - - $wpdb_prepare_args = array( - $prepared_args['offset'], - $prepared_args['number'], - ); + return $dispatch_result; + } - // Filter by tax class. - if ( empty( $prepared_args['class'] ) ) { - $query = sprintf( $query, '' ); - } else { - $class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : ''; - array_unshift( $wpdb_prepare_args, $class ); - $query = sprintf( $query, 'WHERE tax_rate_class = %s' ); + /** + * Register hooks to modify WC REST API response. + */ + public function wcpos_register_wc_rest_api_hooks(): void { + add_filter( 'woocommerce_rest_tax_query', array( $this, 'wcpos_tax_query' ), 10, 2 ); + } + + /** + * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API. + * + * NOTE: tax queries don't have a way to filter the where clause, so to add support for 'include' or 'exclude', + * we can either modify the raw query statement, or fetch all and then filter the response. + * Both are not ideal, but we'll go with the query modification for now. + * + * @param array $prepared_args Array of arguments for $wpdb->get_results(). + * @param WP_REST_Request $request The current request. + * + * @return array + */ + public function wcpos_tax_query( array $prepared_args, WP_REST_Request $request ) { + // Check for wcpos_include/wcpos_exclude parameter. + if ( isset( $request['wcpos_include'] ) || isset( $request['wcpos_exclude'] ) ) { + // Add a custom WHERE clause to the query. + add_filter( 'query', array( $this, 'wcpos_tax_add_include_exclude_to_sql' ), 10, 1 ); } - // Query taxes. - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - $results = $wpdb->get_results( - $wpdb->prepare( - $query, - $wpdb_prepare_args - ) - ); - // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + return $prepared_args; + } - $taxes = array(); - foreach ( $results as $tax ) { - $data = $this->prepare_item_for_response( $tax, $request ); - $taxes[] = $this->prepare_response_for_collection( $data ); - } + /** + * Filters the database query. + * + * This is dangeous filter because it applies to every db query after 'woocommerce_rest_tax_query' is called. + * There will be one query for the tax rates table, then many other possible queries for other tables, then once + * more query for the tax rates table to get the count. + * + * NOTE: the count query is just a str_replace on the original query, so we can run once and then remove. + * + * @param string $query Database query. + */ + public function wcpos_tax_add_include_exclude_to_sql( $query ) { + global $wpdb; - $response = rest_ensure_response( $taxes ); - - $per_page = (int) $prepared_args['number']; - $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); - - // Unset LIMIT args. - array_splice( $wpdb_prepare_args, -2 ); - - // Count query. - $query = str_replace( - array( - 'SELECT *', - 'LIMIT %d, %d', - ), - array( - 'SELECT COUNT(*)', - '', - ), - $query - ); + if ( strpos( $query, "{$wpdb->prefix}woocommerce_tax_rates" ) !== false ) { + // remove the filter so it doesn't run again + remove_filter( 'query', array( $this, 'wcpos_tax_add_include_exclude_to_sql' ), 10, 1 ); - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - $total_taxes = (int) $wpdb->get_var( empty( $wpdb_prepare_args ) ? $query : $wpdb->prepare( $query, $wpdb_prepare_args ) ); - // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + // Handle include IDs. + if ( ! empty( $this->wcpos_request['wcpos_include'] ) ) { + $include_ids = array_map( 'intval', (array) $this->wcpos_request['wcpos_include'] ); + $ids_format = implode( ',', $include_ids ); + $where_in = "{$wpdb->prefix}woocommerce_tax_rates.tax_rate_id IN ($ids_format)"; - // Calculate totals. - $response->header( 'X-WP-Total', $total_taxes ); - $max_pages = ceil( $total_taxes / $per_page ); - $response->header( 'X-WP-TotalPages', (int) $max_pages ); + $query = $this->wcpos_insert_tax_where_clause( $query, $where_in ); + } - $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); - if ( $page > 1 ) { - $prev_page = $page - 1; - if ( $prev_page > $max_pages ) { - $prev_page = $max_pages; + // Handle exclude IDs. + if ( ! empty( $this->wcpos_request['wcpos_exclude'] ) ) { + $exclude_ids = array_map( 'intval', (array) $this->wcpos_request['wcpos_exclude'] ); + $ids_format = implode( ',', $exclude_ids ); + $where_not_in = "{$wpdb->prefix}woocommerce_tax_rates.tax_rate_id NOT IN ($ids_format)"; + + $query = $this->wcpos_insert_tax_where_clause( $query, $where_not_in ); } - $prev_link = add_query_arg( 'page', $prev_page, $base ); - $response->link_header( 'prev', $prev_link ); - } - if ( $max_pages > $page ) { - $next_page = $page + 1; - $next_link = add_query_arg( 'page', $next_page, $base ); - $response->link_header( 'next', $next_link ); } - return $response; + return $query; } /** - * Dispatch request to parent controller, or override if needed. + * Filters the database query. * - * @param mixed $dispatch_result Dispatch result, will be used if not empty. - * @param WP_REST_Request $request Request used to generate the response. - * @param string $route Route matched for the request. - * @param array $handler Route handler used for the request. + * @param string $query Database query. + * @param string $condition WHERE condition to insert. + * + * @return string */ - public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $request, $route, $handler ) { - $this->wcpos_register_wc_rest_api_hooks(); - $params = $request->get_params(); + private function wcpos_insert_tax_where_clause( $query, $condition ) { + global $wpdb; - // Optimised query for getting all product IDs - if ( isset( $params['posts_per_page'] ) && -1 == $params['posts_per_page'] && isset( $params['fields'] ) ) { - $dispatch_result = $this->wcpos_get_all_posts( $params['fields'] ); + if ( strpos( $query, 'WHERE' ) !== false ) { + // Insert condition in existing WHERE clause + $query = str_replace( 'WHERE', "WHERE $condition AND", $query ); + } else { + // Insert WHERE clause before ORDER BY or at the end of the query + $pos = strpos( $query, 'ORDER BY' ); + if ( $pos !== false ) { + $query = substr_replace( $query, " WHERE $condition ", $pos, 0 ); + } else { + $query .= " WHERE $condition"; + } } - return $dispatch_result; - } - - /** - * Register hooks to modify WC REST API response. - */ - public function wcpos_register_wc_rest_api_hooks(): void { + return $query; } /** @@ -207,7 +171,8 @@ public function wcpos_register_wc_rest_api_hooks(): void { * * @return bool|WP_Error */ - public function get_item_permissions_check( $request ) { // no typing when overriding parent method + public function get_item_permissions_check( $request ) { + // no typing when overriding parent method $permission = parent::get_item_permissions_check( $request ); if ( ! $permission && current_user_can( 'read_private_products' ) ) { @@ -227,14 +192,20 @@ public function get_item_permissions_check( $request ) { // no typing when overr public function wcpos_get_all_posts( array $fields = array() ): array { global $wpdb; - $results = $wpdb->get_results( ' + $results = $wpdb->get_results( + ' SELECT tax_rate_id as id FROM ' . $wpdb->prefix . 'woocommerce_tax_rates - ', ARRAY_A ); + ', + ARRAY_A + ); // Convert array of arrays into array of strings (ids) - $all_ids = array_map( function( $item ) { - return \strval( $item['id'] ); - }, $results ); + $all_ids = array_map( + function ( $item ) { + return \strval( $item['id'] ); + }, + $results + ); return array_map( array( $this, 'wcpos_format_id' ), $all_ids ); } diff --git a/tests/includes/API/Test_Product_Categories_Controller.php b/tests/includes/API/Test_Product_Categories_Controller.php index 9a7a809..fb7dc73 100644 --- a/tests/includes/API/Test_Product_Categories_Controller.php +++ b/tests/includes/API/Test_Product_Categories_Controller.php @@ -108,4 +108,80 @@ public function test_product_category_response_contains_uuid_meta_data(): void { $this->assertEquals( 200, $response->get_status() ); $this->assertTrue( Uuid::isValid( $data['uuid'] ), 'The UUID value is not valid.' ); } + + /** + * + */ + public function test_product_category_includes() { + $cat1 = ProductHelper::create_product_category( 'Music' ); + $cat2 = ProductHelper::create_product_category( 'Clothes' ); + $request = $this->wp_rest_get_request( '/wcpos/v1/products/categories' ); + $request->set_param( 'include', $cat1['term_id'] ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( 1, \count( $data ) ); + + $this->assertEquals( $cat1['term_id'], $data[0]['id'] ); + } + + /** + * NOTE: There is one category installed in the test setup. + */ + public function test_product_category_excludes() { + $cat1 = ProductHelper::create_product_category( 'Music' ); + $cat2 = ProductHelper::create_product_category( 'Clothes' ); + $request = $this->wp_rest_get_request( '/wcpos/v1/products/categories' ); + $request->set_param( 'exclude', $cat1['term_id'] ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( 2, \count( $data ) ); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertNotContains( $cat1['term_id'], $ids ); + $this->assertContains( $cat2['term_id'], $ids ); + } + + /** + * + */ + public function test_product_category_search_with_includes() { + $cat1 = ProductHelper::create_product_category( 'Music1' ); + $cat2 = ProductHelper::create_product_category( 'Music2' ); + $request = $this->wp_rest_get_request( '/wcpos/v1/products/categories' ); + $request->set_param( 'include', $cat1['term_id'] ); + $request->set_param( 'search', 'Music' ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( 1, \count( $data ) ); + + $this->assertEquals( $cat1['term_id'], $data[0]['id'] ); + } + + /** + * NOTE: There is one category installed in the test setup. + */ + public function test_product_category_search_with_excludes() { + $cat1 = ProductHelper::create_product_category( 'Music1' ); + $cat2 = ProductHelper::create_product_category( 'Music2' ); + $request = $this->wp_rest_get_request( '/wcpos/v1/products/categories' ); + $request->set_param( 'exclude', $cat1['term_id'] ); + $request->set_param( 'search', 'Music' ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( 1, \count( $data ) ); + + $this->assertEquals( $cat2['term_id'], $data[0]['id'] ); + } } diff --git a/tests/includes/API/Test_Product_Tags_Controller.php b/tests/includes/API/Test_Product_Tags_Controller.php index eed2756..7977fd7 100644 --- a/tests/includes/API/Test_Product_Tags_Controller.php +++ b/tests/includes/API/Test_Product_Tags_Controller.php @@ -103,4 +103,78 @@ public function test_product_tag_response_contains_uuid_meta_data(): void { $this->assertEquals( 200, $response->get_status() ); $this->assertTrue( Uuid::isValid( $data['uuid'] ), 'The UUID value is not valid.' ); } + + /** + * + */ + public function test_product_tag_includes() { + $tag1 = ProductHelper::create_product_tag( 'Music' ); + $tag2 = ProductHelper::create_product_tag( 'Clothes' ); + $request = $this->wp_rest_get_request( '/wcpos/v1/products/tags' ); + $request->set_param( 'include', $tag1['term_id'] ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( 1, \count( $data ) ); + + $this->assertEquals( $tag1['term_id'], $data[0]['id'] ); + } + + /** + * + */ + public function test_product_tag_excludes() { + $tag1 = ProductHelper::create_product_tag( 'Music' ); + $tag2 = ProductHelper::create_product_tag( 'Clothes' ); + $request = $this->wp_rest_get_request( '/wcpos/v1/products/tags' ); + $request->set_param( 'exclude', $tag1['term_id'] ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( 1, \count( $data ) ); + + $this->assertEquals( $tag2['term_id'], $data[0]['id'] ); + } + + /** + * + */ + public function test_product_tag_search_with_includes() { + $tag1 = ProductHelper::create_product_tag( 'Music1' ); + $tag2 = ProductHelper::create_product_tag( 'Music2' ); + $request = $this->wp_rest_get_request( '/wcpos/v1/products/tags' ); + $request->set_param( 'include', $tag1['term_id'] ); + $request->set_param( 'search', 'Music' ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( 1, \count( $data ) ); + + $this->assertEquals( $tag1['term_id'], $data[0]['id'] ); + } + + /** + * + */ + public function test_product_tag_search_with_excludes() { + $tag1 = ProductHelper::create_product_tag( 'Music1' ); + $tag2 = ProductHelper::create_product_tag( 'Music2' ); + $request = $this->wp_rest_get_request( '/wcpos/v1/products/tags' ); + $request->set_param( 'exclude', $tag1['term_id'] ); + $request->set_param( 'search', 'Music' ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( 1, \count( $data ) ); + + $this->assertEquals( $tag2['term_id'], $data[0]['id'] ); + } } diff --git a/tests/includes/API/Test_Product_Variations_Controller.php b/tests/includes/API/Test_Product_Variations_Controller.php index df44c07..bc04062 100644 --- a/tests/includes/API/Test_Product_Variations_Controller.php +++ b/tests/includes/API/Test_Product_Variations_Controller.php @@ -680,7 +680,7 @@ function () { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 2, \count( $data ) ); - $this->assertEquals( 'some_string', $data[0]['barcode'] ); + $this->assertEquals( 'some_string', $data[1]['barcode'] ); } /** @@ -810,7 +810,7 @@ public function test_all_variation_with_excludes() { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 4, \count( $data ) ); - $this->assertEqualsCanonicalizing( array( $variation_ids1[1], $variation_ids2[0], $variation_ids2[1], $variation_ids3[0] ), $ids ); + $this->assertEqualsCanonicalizing( array( $variation_ids2[0], $variation_ids2[1], $variation_ids3[0], $variation_ids3[1] ), $ids ); } /** @@ -828,7 +828,7 @@ public function test_all_variation_search_with_includes() { $request->set_query_params( array( 'include' => array( $variation_ids1[0], $variation_ids1[1] ), - 'search' => 'DUMMY SKU VARIABLE SMALL', + 'search' => 'SMALL', ) ); $response = $this->server->dispatch( $request ); @@ -854,7 +854,7 @@ public function test_all_variation_search_with_excludes() { $request->set_query_params( array( 'exclude' => array( $variation_ids1[0], $variation_ids1[1] ), - 'search' => 'DUMMY SKU VARIABLE SMALL', + 'search' => 'SMALL', ) ); $response = $this->server->dispatch( $request ); diff --git a/tests/includes/API/Test_Taxes_Controller.php b/tests/includes/API/Test_Taxes_Controller.php index ec15b68..005668c 100644 --- a/tests/includes/API/Test_Taxes_Controller.php +++ b/tests/includes/API/Test_Taxes_Controller.php @@ -109,10 +109,8 @@ public function test_product_category_api_get_all_ids(): void { /** * The WC REST API does not support the include param. * This test is to ensure that the include param is supported in the WCPOS API. - * - * @TODO - I will need to adjust the HEADER counts as well */ - public function test_include_param(): void { + public function test_include_and_exclude_param(): void { $tax_id1 = TaxHelper::create_tax_rate( array( 'country' => 'US', @@ -164,5 +162,89 @@ public function test_include_param(): void { $ids = wp_list_pluck( $data, 'id' ); $this->assertEqualsCanonicalizing( array( $tax_id2, $tax_id4 ), $ids ); + + $request = $this->wp_rest_get_request( '/wcpos/v1/taxes' ); + $request->set_param( 'exclude', array( $tax_id2, $tax_id4 ) ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 3, \count( $data ) ); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertEqualsCanonicalizing( array( $tax_id1, $tax_id3, $tax_id5 ), $ids ); + } + + /** + * + */ + public function test_include_and_exclude_param_with_class(): void { + $tax_id1 = TaxHelper::create_tax_rate( + array( + 'country' => 'US', + 'state' => 'NY', + 'rate' => '8.375', + 'name' => 'NY Tax', + 'class' => 'reduced-rate', + ) + ); + $tax_id2 = TaxHelper::create_tax_rate( + array( + 'country' => 'US', + 'state' => 'CA', + 'rate' => '7.25', + 'name' => 'CA Tax', + ) + ); + $tax_id3 = TaxHelper::create_tax_rate( + array( + 'country' => 'US', + 'state' => 'FL', + 'rate' => '6.00', + 'name' => 'FL Tax', + 'class' => 'reduced-rate', + ) + ); + $tax_id4 = TaxHelper::create_tax_rate( + array( + 'country' => 'US', + 'state' => 'TX', + 'rate' => '6.25', + 'name' => 'TX Tax', + ) + ); + $tax_id5 = TaxHelper::create_tax_rate( + array( + 'country' => 'US', + 'state' => 'WA', + 'rate' => '6.50', + 'name' => 'WA Tax', + 'class' => 'reduced-rate', + ) + ); + + $request = $this->wp_rest_get_request( '/wcpos/v1/taxes' ); + $request->set_param( 'include', array( $tax_id1, $tax_id2 ) ); + $request->set_param( 'class', 'reduced-rate' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, \count( $data ) ); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertEqualsCanonicalizing( array( $tax_id1 ), $ids ); + + $request = $this->wp_rest_get_request( '/wcpos/v1/taxes' ); + $request->set_param( 'exclude', array( $tax_id1, $tax_id2 ) ); + $request->set_param( 'class', 'reduced-rate' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, \count( $data ) ); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertEqualsCanonicalizing( array( $tax_id3, $tax_id5 ), $ids ); } }