PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.17
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.17
1.10.25 1.10.24 1.10.23 1.10.22 1.10.21 1.10.20 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 All 169 releases
← All changes | includes/API/V2/Variations_Controller.php +88 -23 1.10.20 → 1.10.17 View file →
@@ -8,14 +8,15 @@
8 8 namespace WCPOS\WooCommercePOS\API\V2;
9 9
10 10 use WC_Product_Variation;
11 11 use WC_REST_Product_Variations_Controller;
12 +use WCPOS\WooCommercePOS\Services\Barcode_Field;
12 13 use WCPOS\WooCommercePOS\Sync\Api;
13 14 use WCPOS\WooCommercePOS\Sync\Collection_Rules;
14 15 use WCPOS\WooCommercePOS\Sync\Collection_Rules_Plan;
15 16 use WCPOS\WooCommercePOS\Sync\Digest_Index;
16 17 use WCPOS\WooCommercePOS\Sync\Endpoint_Permissions;
17 -use WCPOS\WooCommercePOS\Sync\Product_Search;
18 +use WCPOS\WooCommercePOS\Sync\Pos_Visibility;
18 19 use WCPOS\WooCommercePOS\Sync\Product_Serializer;
19 20 use WP_Error;
20 21 use WP_Query;
21 22 use WP_REST_Request;
@@ -58,13 +59,12 @@
58 59 */
59 60 private const WCPOS_SORT_PARAM_MAP = array(
60 61 'orderby' => 'orderby',
61 62 'order' => 'order',
62 - 'search' => 'search',
63 63 );
64 64
65 65 /**
66 - * The request whose declared rules wrap the collection query.
66 + * The request whose declared sort `wcpos_posts_clauses()` applies.
67 67 *
68 68 * @var null|WP_REST_Request
69 69 */
70 70 private $wcpos_sort_request = null;
@@ -73,8 +73,9 @@
73 73
74 74 private const MAX_SKU_LENGTH = 4096;
75 75 private const MAX_SKU_TERMS = 100;
76 76 private const MAX_SEARCH_LENGTH = 256;
77 + private const MAX_SEARCH_TERMS = 10;
77 78 private const MAX_PAGE = 1000;
78 79
79 80
80 81 public function register_routes(): void {
@@ -151,8 +152,49 @@
151 152 */
152 153 $args['post_type'] = $this->post_type;
153 154
154 155 /*
156 + * `search` means the barcode CARRIERS here, not the post title.
157 + *
158 + * WooCommerce maps `search` onto `s`, which searches post_title/content — useless for a
159 + * variation, whose title is a generated attribute string. The POS searches what a cashier
160 + * actually types or scans: the SKU and whichever meta key the store configured as its
161 + * barcode field (`Barcode_Field::search_keys()`). Every term must match at least one carrier.
162 + *
163 + * `sku` is left to WooCommerce: its own exact/comma-list handling is what the
164 + * sku-beats-search precedence rule relies on.
165 + */
166 + $search = (string) ( $request->get_param( 'search' ) ?? '' );
167 + if ( '' !== $sku ) {
168 + // SKU is an exact lookup and outranks a fuzzy one; leaving WooCommerce's post-title
169 + // `s` in place would AND the two and return nothing.
170 + unset( $args['s'] );
171 + }
172 + if ( '' !== $search && '' === $sku ) {
173 + unset( $args['s'] );
174 + $args['wcpos_variation_search'] = true;
175 + $terms = preg_split( '/[\s\p{Z}\p{C}]+/u', trim( $search ), -1, PREG_SPLIT_NO_EMPTY );
176 + if ( false === $terms ) {
177 + $terms = array();
178 + }
179 + $carriers = array( 'relation' => 'AND' );
180 + foreach ( $terms as $term ) {
181 + $term_carriers = array( 'relation' => 'OR' );
182 + foreach ( Barcode_Field::search_keys() as $key ) {
183 + $term_carriers[] = array(
184 + 'key' => $key,
185 + 'value' => $term,
186 + 'compare' => 'LIKE',
187 + );
188 + }
189 + $carriers[] = $term_carriers;
190 + }
191 + if ( 1 < \count( $carriers ) ) {
192 + $args['meta_query'] = $this->add_meta_query( $args, $carriers ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
193 + }
194 + }
195 +
196 + /*
155 197 * This route only ever offers what the store owner has for sale — on EVERY lane, including
156 198 * `include`.
157 199 *
158 200 * WooCommerce's Enabled checkbox on the variation metabox writes `post_status = private`
@@ -171,25 +213,38 @@
171 213 * Set after `parent::prepare_objects_query()` so an explicit `status` param cannot widen it.
172 214 */
173 215 $args['post_status'] = 'publish';
174 216
217 + /*
218 + * Leg-3 (ADR 0014 WP-M5): POS-hidden (`online_only`) variations are never served. As a
219 + * query exclusion rather than a post-hoc filter of the result, so paging and totals count
220 + * the same set the client is allowed to see.
221 + *
222 + * Through the helper, NOT a raw `post__not_in` merge: `parent::prepare_objects_query()`
223 + * maps `include` to `post__in`, and WP_Query IGNORES `post__not_in` when `post__in` is
224 + * present — so `?search=X&include=<hidden id>` would have served a hidden variation.
225 + * `apply_to_wp_query_args()` already owns that trap: it intersects `post__in` with the
226 + * hidden set and pins an empty intersection to `array( 0 )`.
227 + */
228 + $args = ( new Pos_Visibility() )->apply_to_wp_query_args( $args, Pos_Visibility::VARIATIONS );
229 +
230 + /*
231 + * The POS sorts on fields WooCommerce does not offer as orderby values. They are
232 + * declared in Sync\Collection_Rules and projected into get_collection_params()
233 + * below — without that, `orderby=sku` is rejected by REST argument validation
234 + * before anything here runs.
235 + *
236 + * They are applied as SQL clauses, NOT as `meta_key` + `orderby => meta_value`:
237 + * that pair INNER JOINs postmeta and drops every variation with no value for the
238 + * key, so the sort silently filtered. `wcpos_posts_clauses()` LEFT JOINs instead
239 + * and orders the meta-less rows last.
240 + */
175 241 $this->wcpos_sort_request = $request;
176 - $plan = Collection_Rules::for_request( 'variations', $request, self::WCPOS_SORT_PARAM_MAP );
177 - $args = $plan->filter( Collection_Rules_Plan::HOOK_PREPARE_ARGS, $args );
242 + add_filter( 'posts_clauses', array( $this, 'wcpos_posts_clauses' ), 10, 2 );
178 243
179 244 return $args;
180 245 }
181 246
182 - /** Apply the same rule topology to discovery, collection pages, and named includes. */
183 - protected function get_objects( $query_args ) {
184 - $plan = Collection_Rules::for_request( 'variations', $this->wcpos_sort_request, self::WCPOS_SORT_PARAM_MAP );
185 - return $plan->around(
186 - function () use ( $query_args ) {
187 - return parent::get_objects( $query_args );
188 - }
189 - );
190 - }
191 -
192 247 /**
193 248 * GET /variations — the flat collection's three lanes, one response shape.
194 249 *
195 250 * `?sku=`/`?search=` discovers by barcode carrier; a bare request serves one
@@ -395,10 +450,13 @@
395 450 }
396 451 if ( self::MAX_SEARCH_LENGTH < $characters ) {
397 452 return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'search must not exceed 256 characters', array( 'status' => 400 ) );
398 453 }
399 - $terms = Collection_Rules::search_terms( trim( $search ) );
400 - if ( Collection_Rules::rules( 'variations' )['search']['term_cap'] < \count( $terms ) ) {
454 + $terms = preg_split( '/[\s\p{Z}\p{C}]+/u', trim( $search ), -1, PREG_SPLIT_NO_EMPTY );
455 + if ( false === $terms ) {
456 + return new WP_Error( 'woocommerce_pos_variations_search_invalid', 'search must be valid UTF-8', array( 'status' => 400 ) );
457 + }
458 + if ( self::MAX_SEARCH_TERMS < \count( $terms ) ) {
401 459 return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'search must not contain more than 10 whitespace-separated terms', array( 'status' => 400 ) );
402 460 }
403 461 }
404 462
@@ -418,9 +476,8 @@
418 476 *
419 477 * @param array $clauses Associative array of the clauses for the query.
420 478 * @param WP_Query $wp_query The WP_Query instance.
421 479 *
422 - * @deprecated Collection Rules now installs this behavior; retained for Pro callers.
423 480 * @return array
424 481 */
425 482 public function wcpos_posts_clauses( array $clauses, WP_Query $wp_query ): array {
426 483 if ( null === $this->wcpos_sort_request ) {
@@ -466,13 +523,13 @@
466 523 * De-duplicate variation searches joined through matching meta rows.
467 524 *
468 525 * @param string $groupby Existing GROUP BY clause.
469 526 * @param WP_Query $query Query being filtered.
470 - *
471 - * @deprecated Collection Rules owns variation grouping.
472 527 */
473 528 public function group_search_results( string $groupby, WP_Query $query ): string {
474 - return Product_Search::variation_groupby( $groupby, $query->query_vars );
529 + global $wpdb;
530 +
531 + return ! empty( $query->query_vars['wcpos_variation_search'] ) ? "{$wpdb->posts}.ID" : $groupby;
475 532 }
476 533
477 534 /**
478 535 * Does this discovery request still carry a term after normalization?
@@ -486,9 +543,12 @@
486 543 return true;
487 544 }
488 545
489 546 $search = (string) ( $request->get_param( 'search' ) ?? '' );
490 - $terms = Collection_Rules::search_terms( trim( $search ) );
547 + $terms = preg_split( '/[\s\p{Z}\p{C}]+/u', trim( $search ), -1, PREG_SPLIT_NO_EMPTY );
548 + if ( false === $terms ) {
549 + return false;
550 + }
491 551
492 552 return array() !== $terms;
493 553 }
494 554
@@ -563,9 +623,14 @@
563 623 ),
564 624 );
565 625 }
566 626
567 - $results = $this->get_objects( $query_args );
627 + add_filter( 'posts_groupby', array( $this, 'group_search_results' ), 10, 2 );
628 + try {
629 + $results = $this->get_objects( $query_args );
630 + } finally {
631 + remove_filter( 'posts_groupby', array( $this, 'group_search_results' ), 10 );
632 + }
568 633
569 634 $ids = array();
570 635 foreach ( $results['objects'] as $object ) {
571 636 if ( $object instanceof WC_Product_Variation ) {