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/V2/Variations_Controller.php +78 -95 1.10.31.10.18 View file →
@@ -8,13 +8,14 @@
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;
13 12 use WCPOS\WooCommercePOS\Sync\Api;
13 +use WCPOS\WooCommercePOS\Sync\Collection_Rules;
14 +use WCPOS\WooCommercePOS\Sync\Collection_Rules_Plan;
14 15 use WCPOS\WooCommercePOS\Sync\Digest_Index;
15 16 use WCPOS\WooCommercePOS\Sync\Endpoint_Permissions;
16 -use WCPOS\WooCommercePOS\Sync\Pos_Visibility;
17 +use WCPOS\WooCommercePOS\Sync\Product_Search;
17 18 use WCPOS\WooCommercePOS\Sync\Product_Serializer;
18 19 use WP_Error;
19 20 use WP_Query;
20 21 use WP_REST_Request;
@@ -49,14 +50,31 @@
49 50 * (`documents[].{id,parent_id,payload,_rxdb_digest}`), POS visibility, the barcode carrier
50 51 * search, and the request bounds. Everything else is WooCommerce's.
51 52 */
52 53 class Variations_Controller extends WC_REST_Product_Variations_Controller {
54 + /**
55 + * Request keys the variation Collection Rules plan reads on this lane.
56 + *
57 + * @var array
58 + */
59 + private const WCPOS_SORT_PARAM_MAP = array(
60 + 'orderby' => 'orderby',
61 + 'order' => 'order',
62 + 'search' => 'search',
63 + );
64 +
65 + /**
66 + * The request whose declared rules wrap the collection query.
67 + *
68 + * @var null|WP_REST_Request
69 + */
70 + private $wcpos_sort_request = null;
71 +
53 72 use Endpoint_Permissions;
54 73
55 74 private const MAX_SKU_LENGTH = 4096;
56 75 private const MAX_SKU_TERMS = 100;
57 76 private const MAX_SEARCH_LENGTH = 256;
58 - private const MAX_SEARCH_TERMS = 10;
59 77 private const MAX_PAGE = 1000;
60 78
61 79
62 80 public function register_routes(): void {
@@ -133,44 +151,8 @@
133 151 */
134 152 $args['post_type'] = $this->post_type;
135 153
136 154 /*
137 - * `search` means the barcode CARRIERS here, not the post title.
138 - *
139 - * WooCommerce maps `search` onto `s`, which searches post_title/content — useless for a
140 - * variation, whose title is a generated attribute string. The POS searches what a cashier
141 - * actually types or scans: the SKU and whichever meta key the store configured as its
142 - * barcode field (`Barcode_Field::search_keys()`). Any term matching any carrier wins,
143 - * which is the semantics the previous hand-rolled SQL had and the specs pin.
144 - *
145 - * `sku` is left to WooCommerce: its own exact/comma-list handling is what the
146 - * sku-beats-search precedence rule relies on.
147 - */
148 - $search = (string) ( $request->get_param( 'search' ) ?? '' );
149 - if ( '' !== $sku ) {
150 - // SKU is an exact lookup and outranks a fuzzy one; leaving WooCommerce's post-title
151 - // `s` in place would AND the two and return nothing.
152 - unset( $args['s'] );
153 - }
154 - if ( '' !== $search && '' === $sku ) {
155 - unset( $args['s'] );
156 - $args['wcpos_variation_search'] = true;
157 - $carriers = array( 'relation' => 'OR' );
158 - foreach ( (array) preg_split( '/\s+/', trim( $search ), -1, PREG_SPLIT_NO_EMPTY ) as $term ) {
159 - foreach ( Barcode_Field::search_keys() as $key ) {
160 - $carriers[] = array(
161 - 'key' => $key,
162 - 'value' => $term,
163 - 'compare' => 'LIKE',
164 - );
165 - }
166 - }
167 - if ( 1 < \count( $carriers ) ) {
168 - $args['meta_query'] = $this->add_meta_query( $args, $carriers ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
169 - }
170 - }
171 -
172 - /*
173 155 * This route only ever offers what the store owner has for sale — on EVERY lane, including
174 156 * `include`.
175 157 *
176 158 * WooCommerce's Enabled checkbox on the variation metabox writes `post_status = private`
@@ -189,52 +171,23 @@
189 171 * Set after `parent::prepare_objects_query()` so an explicit `status` param cannot widen it.
190 172 */
191 173 $args['post_status'] = 'publish';
192 174
193 - /*
194 - * Leg-3 (ADR 0014 WP-M5): POS-hidden (`online_only`) variations are never served. As a
195 - * query exclusion rather than a post-hoc filter of the result, so paging and totals count
196 - * the same set the client is allowed to see.
197 - *
198 - * Through the helper, NOT a raw `post__not_in` merge: `parent::prepare_objects_query()`
199 - * maps `include` to `post__in`, and WP_Query IGNORES `post__not_in` when `post__in` is
200 - * present — so `?search=X&include=<hidden id>` would have served a hidden variation.
201 - * `apply_to_wp_query_args()` already owns that trap: it intersects `post__in` with the
202 - * hidden set and pins an empty intersection to `array( 0 )`.
203 - */
204 - $args = ( new Pos_Visibility() )->apply_to_wp_query_args( $args, Pos_Visibility::VARIATIONS );
175 + $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 );
205 178
206 - /*
207 - * The POS sorts on fields WooCommerce does not offer as orderby values. They are declared
208 - * in get_collection_params() below — without that, `orderby=sku` is rejected by REST
209 - * argument validation before this switch ever runs.
210 - */
211 - if ( isset( $request['orderby'] ) ) {
212 - switch ( $request['orderby'] ) {
213 - case 'sku':
214 - $args['meta_key'] = '_sku'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
215 - $args['orderby'] = 'meta_value';
179 + return $args;
180 + }
216 181
217 - break;
218 - case 'barcode':
219 - $args['meta_key'] = Barcode_Field::orderby_key(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
220 - $args['orderby'] = 'meta_value';
221 -
222 - break;
223 - case 'stock_quantity':
224 - $args['meta_key'] = '_stock'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
225 - $args['orderby'] = 'meta_value_num';
226 -
227 - break;
228 - case 'stock_status':
229 - $args['meta_key'] = '_stock_status'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
230 - $args['orderby'] = 'meta_value';
231 -
232 - break;
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 );
233 188 }
234 - }
235 -
236 - return $args;
189 + );
237 190 }
238 191
239 192 /**
240 193 * GET /variations — the flat collection's three lanes, one response shape.
@@ -434,13 +387,18 @@
434 387 return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'sku must not contain more than 100 comma-separated terms', array( 'status' => 400 ) );
435 388 }
436 389 } else {
437 390 $search = (string) $request->get_param( 'search' );
438 - if ( self::MAX_SEARCH_LENGTH < \strlen( $search ) ) {
439 - return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'search must not exceed 256 bytes', array( 'status' => 400 ) );
391 + // Unlike mb_strlen(), PCRE is independent of blog_charset and detects malformed UTF-8.
392 + $characters = preg_match_all( '/./us', $search );
393 + if ( false === $characters ) {
394 + return new WP_Error( 'woocommerce_pos_variations_search_invalid', 'search must be valid UTF-8', array( 'status' => 400 ) );
440 395 }
441 - $terms = (array) preg_split( '/\s+/', trim( $search ), -1, PREG_SPLIT_NO_EMPTY );
442 - if ( self::MAX_SEARCH_TERMS < \count( $terms ) ) {
396 + if ( self::MAX_SEARCH_LENGTH < $characters ) {
397 + return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'search must not exceed 256 characters', array( 'status' => 400 ) );
398 + }
399 + $terms = Collection_Rules::search_terms( trim( $search ) );
400 + if ( Collection_Rules::rules( 'variations' )['search']['term_cap'] < \count( $terms ) ) {
443 401 return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'search must not contain more than 10 whitespace-separated terms', array( 'status' => 400 ) );
444 402 }
445 403 }
446 404
@@ -451,8 +409,36 @@
451 409 return true;
452 410 }
453 411
454 412 /**
413 + * Apply the declared POS variation sorts to the SQL clauses.
414 + *
415 + * `posts_clauses` fires for EVERY WP_Query, so the body is guarded by post type and by
416 + * the plan itself — it contributes nothing unless this request claimed one of the
417 + * declared sorts.
418 + *
419 + * @param array $clauses Associative array of the clauses for the query.
420 + * @param WP_Query $wp_query The WP_Query instance.
421 + *
422 + * @deprecated Collection Rules now installs this behavior; retained for Pro callers.
423 + * @return array
424 + */
425 + public function wcpos_posts_clauses( array $clauses, WP_Query $wp_query ): array {
426 + if ( null === $this->wcpos_sort_request ) {
427 + return $clauses;
428 + }
429 +
430 + $post_type = $wp_query->query_vars['post_type'] ?? null;
431 + if ( 'product_variation' !== $post_type && ( ! \is_array( $post_type ) || ! \in_array( 'product_variation', $post_type, true ) ) ) {
432 + return $clauses;
433 + }
434 +
435 + $plan = Collection_Rules::for_request( 'variations', $this->wcpos_sort_request, self::WCPOS_SORT_PARAM_MAP );
436 +
437 + return $plan->filter( Collection_Rules_Plan::HOOK_POSTS_CLAUSES, $clauses, $wp_query );
438 + }
439 +
440 + /**
455 441 * WooCommerce's collection params, plus the sort keys the POS grids offer.
456 442 *
457 443 * `orderby` is a validated enum. Appending here is what lets `prepare_objects_query()` act on
458 444 * these four — otherwise the request 400s during argument validation and the switch is dead
@@ -459,8 +445,9 @@
459 445 * code. 1.9.x extended the same enum for the same reason.
460 446 */
461 447 public function get_collection_params() {
462 448 $params = parent::get_collection_params();
449 + $params['search']['sanitize_callback'] = 'rest_sanitize_request_arg';
463 450
464 451 if ( isset( $params['orderby']['enum'] ) && \is_array( $params['orderby']['enum'] ) ) {
465 452 $params['orderby']['enum'] = array_values(
466 453 array_unique(
@@ -465,9 +452,9 @@
465 452 $params['orderby']['enum'] = array_values(
466 453 array_unique(
467 454 array_merge(
468 455 $params['orderby']['enum'],
469 - array( 'sku', 'barcode', 'stock_quantity', 'stock_status' )
456 + Collection_Rules::orderby_enum( 'variations' )
470 457 )
471 458 )
472 459 );
473 460 }
@@ -479,13 +466,13 @@
479 466 * De-duplicate variation searches joined through matching meta rows.
480 467 *
481 468 * @param string $groupby Existing GROUP BY clause.
482 469 * @param WP_Query $query Query being filtered.
470 + *
471 + * @deprecated Collection Rules owns variation grouping.
483 472 */
484 473 public function group_search_results( string $groupby, WP_Query $query ): string {
485 - global $wpdb;
486 -
487 - return ! empty( $query->query_vars['wcpos_variation_search'] ) ? "{$wpdb->posts}.ID" : $groupby;
474 + return Product_Search::variation_groupby( $groupby, $query->query_vars );
488 475 }
489 476
490 477 /**
491 478 * Does this discovery request still carry a term after normalization?
@@ -499,10 +486,11 @@
499 486 return true;
500 487 }
501 488
502 489 $search = (string) ( $request->get_param( 'search' ) ?? '' );
490 + $terms = Collection_Rules::search_terms( trim( $search ) );
503 491
504 - return array() !== (array) preg_split( '/\s+/', trim( $search ), -1, PREG_SPLIT_NO_EMPTY );
492 + return array() !== $terms;
505 493 }
506 494
507 495 /**
508 496 * One page of the POS-servable variation collection, with its total.
@@ -575,14 +563,9 @@
575 563 ),
576 564 );
577 565 }
578 566
579 - add_filter( 'posts_groupby', array( $this, 'group_search_results' ), 10, 2 );
580 - try {
581 - $results = $this->get_objects( $query_args );
582 - } finally {
583 - remove_filter( 'posts_groupby', array( $this, 'group_search_results' ), 10 );
584 - }
567 + $results = $this->get_objects( $query_args );
585 568
586 569 $ids = array();
587 570 foreach ( $results['objects'] as $object ) {
588 571 if ( $object instanceof WC_Product_Variation ) {