PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
← All changes | includes/API/V1/Products_Controller.php +67 -114 1.10.21.10.18 View file →
@@ -17,11 +17,14 @@
17 17 use WC_Data;
18 18 use WC_Product;
19 19 use WC_Product_Variable;
20 20 use WC_REST_Products_Controller;
21 +use WCPOS\WooCommercePOS\API\Product_Search;
21 22 use WCPOS\WooCommercePOS\Logger;
22 23 use WCPOS\WooCommercePOS\Services\Barcode_Field;
23 24 use WCPOS\WooCommercePOS\Services\Variable_Price_Range;
25 +use WCPOS\WooCommercePOS\Sync\Collection_Rules;
26 +use WCPOS\WooCommercePOS\Sync\Collection_Rules_Plan;
24 27 use WCPOS\WooCommercePOS\Sync\Pos_Visibility;
25 28 use WP_Error;
26 29 use WP_Query;
27 30 use WP_REST_Request;
@@ -59,8 +62,19 @@
59 62 */
60 63 protected $wcpos_request;
61 64
62 65 /**
66 + * Request keys the product Collection Rules plan reads on this lane.
67 + *
68 + * @var array
69 + */
70 + private const WCPOS_SORT_PARAM_MAP = array(
71 + 'orderby' => 'orderby',
72 + 'order' => 'order',
73 + 'search' => 'search',
74 + );
75 +
76 + /**
63 77 * Memoized parent collection params.
64 78 *
65 79 * @var array|null
66 80 */
@@ -81,10 +95,8 @@
81 95
82 96 add_filter( 'woocommerce_rest_prepare_product_object', array( $this, 'wcpos_product_response' ), 10, 3 );
83 97 add_action( 'woocommerce_rest_insert_product_object', array( $this, 'wcpos_insert_product_object' ), 10, 3 );
84 98 add_filter( 'woocommerce_rest_product_object_query', array( $this, 'wcpos_product_query' ), 10, 2 );
85 - add_filter( 'posts_search', array( $this, 'wcpos_posts_search' ), 10, 2 );
86 - add_filter( 'posts_clauses', array( $this, 'wcpos_posts_clauses' ), 10, 2 );
87 99
88 100 /*
89 101 * Check if the request is for all products and if the 'posts_per_page' is set to -1.
90 102 * Optimised query for getting all product IDs.
@@ -96,8 +108,23 @@
96 108 return $dispatch_result;
97 109 }
98 110
99 111 /**
112 + * Apply the collection's declared rules for the duration of a direct read.
113 + *
114 + * @param WP_REST_Request $request Full details about the request.
115 + * @return \WP_Error|\WP_REST_Response
116 + */
117 + public function get_items( $request ) {
118 + $plan = Collection_Rules::for_request( 'products', $request, self::WCPOS_SORT_PARAM_MAP );
119 + return $plan->around(
120 + function () use ( $request ) {
121 + return parent::get_items( $request );
122 + }
123 + );
124 + }
125 +
126 + /**
100 127 * Create a single product.
101 128 *
102 129 * @param WP_REST_Request $request Full details about the request.
103 130 *
@@ -178,8 +205,14 @@
178 205 if ( isset( $params['per_page'] ) && \is_array( $params['per_page'] ) ) {
179 206 $params['per_page']['minimum'] = -1;
180 207 }
181 208
209 + // Search text is literal on every lane: `sanitize_text_field` would strip `%30`
210 + // and blank malformed UTF-8 before the declared search rule ever saw them.
211 + if ( isset( $params['search'] ) && \is_array( $params['search'] ) ) {
212 + $params['search']['sanitize_callback'] = 'rest_sanitize_request_arg';
213 + }
214 +
182 215 if ( ! $this->wcpos_parent_collection_supports_param( 'brand' ) ) {
183 216 $params['brand'] = array(
184 217 'description' => /* translators: REST API collection parameter description. */ __( 'Limit result set to products assigned to brand IDs or slugs, separated by commas.', 'woocommerce-pos' ),
185 218 'type' => 'string',
@@ -218,15 +251,12 @@
218 251 }
219 252
220 253 // Ensure 'orderby' is set and is an array before attempting to modify it.
221 254 if ( isset( $params['orderby']['enum'] ) && \is_array( $params['orderby']['enum'] ) ) {
222 - // Define new sorting options.
223 - $new_sort_options = array(
224 - 'sku',
225 - 'barcode',
226 - 'stock_quantity',
227 - 'stock_status',
228 - );
255 + // The POS sorts are DECLARED once, in Sync\Collection_Rules, and projected here.
256 + // The v2 proxy lane claims the same list, so a sort cannot be advertised on one
257 + // Read Lane and rejected on the other (#1779).
258 + $new_sort_options = Collection_Rules::orderby_enum( 'products' );
229 259 // Merge new options, avoiding duplicates.
230 260 $params['orderby']['enum'] = array_unique( array_merge( $params['orderby']['enum'], $new_sort_options ) );
231 261 }
232 262
@@ -357,50 +387,13 @@
357 387 * @param string $search The search SQL query.
358 388 * @param WP_Query $wp_query The WP_Query instance (passed by reference).
359 389 *
360 390 * @return string
391 + *
392 + * @deprecated Collection Rules now installs this behavior.
361 393 */
362 394 public function wcpos_posts_search( string $search, WP_Query $wp_query ) {
363 - global $wpdb;
364 -
365 - if ( empty( $search ) ) {
366 - return $search; // skip processing - no search term in query.
367 - }
368 -
369 - $q = $wp_query->query_vars;
370 - $n = ! empty( $q['exact'] ) ? '' : '%';
371 - $search_terms = (array) $q['search_terms'];
372 -
373 - // Fields in the main 'posts' table.
374 - $post_fields = array( 'post_title' );
375 -
376 - // Meta fields to search.
377 - $meta_fields = Barcode_Field::search_keys();
378 -
379 - $search_conditions = array();
380 -
381 - foreach ( $search_terms as $term ) {
382 - $term = $n . $wpdb->esc_like( $term ) . $n;
383 -
384 - // Search in post fields.
385 - foreach ( $post_fields as $field ) {
386 - $search_conditions[] = $wpdb->prepare( "({$wpdb->posts}.$field LIKE %s)", $term ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is safe.
387 - }
388 -
389 - // Search in meta fields.
390 - foreach ( $meta_fields as $field ) {
391 - $search_conditions[] = $wpdb->prepare( '(pm1.meta_value LIKE %s AND pm1.meta_key = %s)', $term, $field );
392 - }
393 - }
394 -
395 - if ( ! empty( $search_conditions ) ) {
396 - $search = ' AND (' . implode( ' OR ', $search_conditions ) . ') ';
397 - if ( ! is_user_logged_in() ) {
398 - $search .= " AND ($wpdb->posts.post_password = '') ";
399 - }
400 - }
401 -
402 - return $search;
395 + return Product_Search::posts_search( $search, $wp_query );
403 396 }
404 397
405 398 /**
406 399 * Filters all query clauses at once, for convenience.
@@ -411,26 +404,20 @@
411 404 * @param string[] $clauses Associative array of the clauses for the query.
412 405 * @param WP_Query $wp_query The WP_Query instance (passed by reference).
413 406 */
414 407 public function wcpos_posts_clauses( array $clauses, WP_Query $wp_query ): array {
415 - global $wpdb;
408 + if ( ! isset( $this->wcpos_request ) ) {
409 + return $clauses;
410 + }
416 411
417 - // Handle NULL values in stock_quantity sorting
418 - // By default, MySQL sorts NULLs first in ASC and last in DESC
419 - // We want NULLs to always be last regardless of sort direction.
420 - if ( isset( $this->wcpos_request ) ) {
421 - $orderby = $this->wcpos_request->get_param( 'orderby' );
422 - $order = strtoupper( $this->wcpos_request->get_param( 'order' ) ?? 'ASC' );
412 + $post_type = $wp_query->query_vars['post_type'] ?? null;
413 + if ( 'product' !== $post_type && ( ! \is_array( $post_type ) || ! \in_array( 'product', $post_type, true ) ) ) {
414 + return $clauses;
415 + }
423 416
424 - if ( 'stock_quantity' === $orderby ) {
425 - // Modify ORDER BY to put NULLs last
426 - // Use CASE to assign a sort priority: non-NULL = 0, NULL = 1
427 - // Then sort by the actual value.
428 - $clauses['orderby'] = "{$wpdb->postmeta}.meta_value IS NULL ASC, {$wpdb->postmeta}.meta_value + 0 {$order}";
429 - }
430 - }
417 + $plan = Collection_Rules::for_request( 'products', $this->wcpos_request, self::WCPOS_SORT_PARAM_MAP );
431 418
432 - return $clauses;
419 + return $plan->filter( Collection_Rules_Plan::HOOK_POSTS_CLAUSES, $clauses, $wp_query );
433 420 }
434 421
435 422 /**
436 423 * Filter the query arguments for a request.
@@ -440,19 +427,8 @@
440 427 *
441 428 * @return array $args Key value array of query var to query value.
442 429 */
443 430 public function wcpos_product_query( array $args, WP_REST_Request $request ) {
444 - if ( ! empty( $request['search'] ) ) {
445 - // We need to set the query up for a postmeta join.
446 - add_filter( 'posts_join', array( $this, 'wcpos_posts_join_to_products_search' ), 10, 2 );
447 - add_filter( 'posts_groupby', array( $this, 'wcpos_posts_groupby_product_search' ), 10, 2 );
448 - }
449 -
450 - // if POS only products are enabled, exclude online-only products.
451 - if ( $this->wcpos_pos_only_products_enabled() ) {
452 - add_filter( 'posts_where', array( $this, 'wcpos_posts_where_product_exclude_online_only' ), 10, 2 );
453 - }
454 -
455 431 // Check for wcpos_include/wcpos_exclude parameter.
456 432 if ( isset( $request['wcpos_include'] ) || isset( $request['wcpos_exclude'] ) ) {
457 433 add_filter( 'posts_where', array( $this, 'wcpos_posts_where_product_include_exclude' ), 20, 2 );
458 434 }
@@ -617,17 +593,13 @@
617 593 * @param string $join The JOIN clause of the query.
618 594 * @param WP_Query $query The WP_Query instance (passed by reference).
619 595 *
620 596 * @return string
597 + *
598 + * @deprecated Collection Rules now installs this behavior.
621 599 */
622 600 public function wcpos_posts_join_to_products_search( string $join, WP_Query $query ) {
623 - global $wpdb;
624 -
625 - if ( ! empty( $query->query_vars['s'] ) && false === strpos( $join, 'pm1' ) ) {
626 - $join .= " LEFT JOIN {$wpdb->postmeta} pm1 ON {$wpdb->posts}.ID = pm1.post_id ";
627 - }
628 -
629 - return $join;
601 + return Product_Search::posts_join( $join, $query );
630 602 }
631 603
632 604 /**
633 605 * Filters the GROUP BY clause of the query.
@@ -635,17 +607,13 @@
635 607 * @param string $groupby The GROUP BY clause of the query.
636 608 * @param WP_Query $query The WP_Query instance (passed by reference).
637 609 *
638 610 * @return string
611 + *
612 + * @deprecated Collection Rules now installs this behavior.
639 613 */
640 614 public function wcpos_posts_groupby_product_search( string $groupby, WP_Query $query ) {
641 - global $wpdb;
642 -
643 - if ( ! empty( $query->query_vars['s'] ) ) {
644 - $groupby = "{$wpdb->posts}.ID";
645 - }
646 -
647 - return $groupby;
615 + return Product_Search::posts_groupby( $groupby, $query );
648 616 }
649 617
650 618 /**
651 619 * Filters the WHERE clause of the query.
@@ -656,8 +624,10 @@
656 624 * @param string $where The WHERE clause of the query.
657 625 * @param WP_Query $query The WP_Query instance (passed by reference).
658 626 *
659 627 * @return string
628 + *
629 + * @deprecated Collection Rules now installs this behavior.
660 630 */
661 631 public function wcpos_posts_where_product_exclude_online_only( string $where, WP_Query $query ) {
662 632 global $wpdb;
663 633
@@ -739,33 +709,16 @@
739 709 protected function prepare_objects_query( $request ) {
740 710 $args = parent::prepare_objects_query( $request );
741 711 $args = $this->wcpos_apply_store_api_tax_operator_fallbacks( $args, $request );
742 712
743 - // Add custom 'orderby' options.
744 - if ( isset( $request['orderby'] ) ) {
745 - switch ( $request['orderby'] ) {
746 - case 'sku':
747 - $args['meta_key'] = '_sku';
748 - $args['orderby'] = 'meta_value';
749 -
750 - break;
751 - case 'barcode':
752 - $args['meta_key'] = Barcode_Field::orderby_key();
753 - $args['orderby'] = 'meta_value';
754 -
755 - break;
756 - case 'stock_quantity':
757 - $args['meta_key'] = '_stock';
758 - $args['orderby'] = 'meta_value_num';
759 -
760 - break;
761 - case 'stock_status':
762 - $args['meta_key'] = '_stock_status';
763 - $args['orderby'] = 'meta_value';
764 -
765 - break;
766 - }
767 - }
713 + /*
714 + * The POS sorts (`sku`, `barcode`, `stock_quantity`, `stock_status`) are NOT
715 + * mapped onto `meta_key` + `orderby => meta_value` here any more. That pair
716 + * INNER JOINs postmeta, so it dropped every product with no value for the key —
717 + * a sort acting as a filter (#1779 follow-up). `Sync\Collection_Rules` declares
718 + * them and `wcpos_posts_clauses()` applies them as a LEFT JOIN, on this lane and
719 + * on the v2 proxy alike.
720 + */
768 721
769 722 return $args;
770 723 }
771 724 }