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 +32 -79 1.10.71.10.18 View file →
@@ -8,15 +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;
14 13 use WCPOS\WooCommercePOS\Sync\Collection_Rules;
15 14 use WCPOS\WooCommercePOS\Sync\Collection_Rules_Plan;
16 15 use WCPOS\WooCommercePOS\Sync\Digest_Index;
17 16 use WCPOS\WooCommercePOS\Sync\Endpoint_Permissions;
18 -use WCPOS\WooCommercePOS\Sync\Pos_Visibility;
17 +use WCPOS\WooCommercePOS\Sync\Product_Search;
19 18 use WCPOS\WooCommercePOS\Sync\Product_Serializer;
20 19 use WP_Error;
21 20 use WP_Query;
22 21 use WP_REST_Request;
@@ -59,12 +58,13 @@
59 58 */
60 59 private const WCPOS_SORT_PARAM_MAP = array(
61 60 'orderby' => 'orderby',
62 61 'order' => 'order',
62 + 'search' => 'search',
63 63 );
64 64
65 65 /**
66 - * The request whose declared sort `wcpos_posts_clauses()` applies.
66 + * The request whose declared rules wrap the collection query.
67 67 *
68 68 * @var null|WP_REST_Request
69 69 */
70 70 private $wcpos_sort_request = null;
@@ -73,9 +73,8 @@
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;
78 77 private const MAX_PAGE = 1000;
79 78
80 79
81 80 public function register_routes(): void {
@@ -152,44 +151,8 @@
152 151 */
153 152 $args['post_type'] = $this->post_type;
154 153
155 154 /*
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()`). Any term matching any carrier wins,
162 - * which is the semantics the previous hand-rolled SQL had and the specs pin.
163 - *
164 - * `sku` is left to WooCommerce: its own exact/comma-list handling is what the
165 - * sku-beats-search precedence rule relies on.
166 - */
167 - $search = (string) ( $request->get_param( 'search' ) ?? '' );
168 - if ( '' !== $sku ) {
169 - // SKU is an exact lookup and outranks a fuzzy one; leaving WooCommerce's post-title
170 - // `s` in place would AND the two and return nothing.
171 - unset( $args['s'] );
172 - }
173 - if ( '' !== $search && '' === $sku ) {
174 - unset( $args['s'] );
175 - $args['wcpos_variation_search'] = true;
176 - $carriers = array( 'relation' => 'OR' );
177 - foreach ( (array) preg_split( '/\s+/', trim( $search ), -1, PREG_SPLIT_NO_EMPTY ) as $term ) {
178 - foreach ( Barcode_Field::search_keys() as $key ) {
179 - $carriers[] = array(
180 - 'key' => $key,
181 - 'value' => $term,
182 - 'compare' => 'LIKE',
183 - );
184 - }
185 - }
186 - if ( 1 < \count( $carriers ) ) {
187 - $args['meta_query'] = $this->add_meta_query( $args, $carriers ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
188 - }
189 - }
190 -
191 - /*
192 155 * This route only ever offers what the store owner has for sale — on EVERY lane, including
193 156 * `include`.
194 157 *
195 158 * WooCommerce's Enabled checkbox on the variation metabox writes `post_status = private`
@@ -208,38 +171,25 @@
208 171 * Set after `parent::prepare_objects_query()` so an explicit `status` param cannot widen it.
209 172 */
210 173 $args['post_status'] = 'publish';
211 174
212 - /*
213 - * Leg-3 (ADR 0014 WP-M5): POS-hidden (`online_only`) variations are never served. As a
214 - * query exclusion rather than a post-hoc filter of the result, so paging and totals count
215 - * the same set the client is allowed to see.
216 - *
217 - * Through the helper, NOT a raw `post__not_in` merge: `parent::prepare_objects_query()`
218 - * maps `include` to `post__in`, and WP_Query IGNORES `post__not_in` when `post__in` is
219 - * present — so `?search=X&include=<hidden id>` would have served a hidden variation.
220 - * `apply_to_wp_query_args()` already owns that trap: it intersects `post__in` with the
221 - * hidden set and pins an empty intersection to `array( 0 )`.
222 - */
223 - $args = ( new Pos_Visibility() )->apply_to_wp_query_args( $args, Pos_Visibility::VARIATIONS );
224 -
225 - /*
226 - * The POS sorts on fields WooCommerce does not offer as orderby values. They are
227 - * declared in Sync\Collection_Rules and projected into get_collection_params()
228 - * below — without that, `orderby=sku` is rejected by REST argument validation
229 - * before anything here runs.
230 - *
231 - * They are applied as SQL clauses, NOT as `meta_key` + `orderby => meta_value`:
232 - * that pair INNER JOINs postmeta and drops every variation with no value for the
233 - * key, so the sort silently filtered. `wcpos_posts_clauses()` LEFT JOINs instead
234 - * and orders the meta-less rows last.
235 - */
236 175 $this->wcpos_sort_request = $request;
237 - add_filter( 'posts_clauses', array( $this, 'wcpos_posts_clauses' ), 10, 2 );
176 + $plan = Collection_Rules::for_request( 'variations', $request, self::WCPOS_SORT_PARAM_MAP );
177 + $args = $plan->filter( Collection_Rules_Plan::HOOK_PREPARE_ARGS, $args );
238 178
239 179 return $args;
240 180 }
241 181
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 +
242 192 /**
243 193 * GET /variations — the flat collection's three lanes, one response shape.
244 194 *
245 195 * `?sku=`/`?search=` discovers by barcode carrier; a bare request serves one
@@ -437,13 +387,18 @@
437 387 return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'sku must not contain more than 100 comma-separated terms', array( 'status' => 400 ) );
438 388 }
439 389 } else {
440 390 $search = (string) $request->get_param( 'search' );
441 - if ( self::MAX_SEARCH_LENGTH < \strlen( $search ) ) {
442 - 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 ) );
443 395 }
444 - $terms = (array) preg_split( '/\s+/', trim( $search ), -1, PREG_SPLIT_NO_EMPTY );
445 - 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 ) ) {
446 401 return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'search must not contain more than 10 whitespace-separated terms', array( 'status' => 400 ) );
447 402 }
448 403 }
449 404
@@ -463,8 +418,9 @@
463 418 *
464 419 * @param array $clauses Associative array of the clauses for the query.
465 420 * @param WP_Query $wp_query The WP_Query instance.
466 421 *
422 + * @deprecated Collection Rules now installs this behavior; retained for Pro callers.
467 423 * @return array
468 424 */
469 425 public function wcpos_posts_clauses( array $clauses, WP_Query $wp_query ): array {
470 426 if ( null === $this->wcpos_sort_request ) {
@@ -489,8 +445,9 @@
489 445 * code. 1.9.x extended the same enum for the same reason.
490 446 */
491 447 public function get_collection_params() {
492 448 $params = parent::get_collection_params();
449 + $params['search']['sanitize_callback'] = 'rest_sanitize_request_arg';
493 450
494 451 if ( isset( $params['orderby']['enum'] ) && \is_array( $params['orderby']['enum'] ) ) {
495 452 $params['orderby']['enum'] = array_values(
496 453 array_unique(
@@ -509,13 +466,13 @@
509 466 * De-duplicate variation searches joined through matching meta rows.
510 467 *
511 468 * @param string $groupby Existing GROUP BY clause.
512 469 * @param WP_Query $query Query being filtered.
470 + *
471 + * @deprecated Collection Rules owns variation grouping.
513 472 */
514 473 public function group_search_results( string $groupby, WP_Query $query ): string {
515 - global $wpdb;
516 -
517 - return ! empty( $query->query_vars['wcpos_variation_search'] ) ? "{$wpdb->posts}.ID" : $groupby;
474 + return Product_Search::variation_groupby( $groupby, $query->query_vars );
518 475 }
519 476
520 477 /**
521 478 * Does this discovery request still carry a term after normalization?
@@ -529,10 +486,11 @@
529 486 return true;
530 487 }
531 488
532 489 $search = (string) ( $request->get_param( 'search' ) ?? '' );
490 + $terms = Collection_Rules::search_terms( trim( $search ) );
533 491
534 - return array() !== (array) preg_split( '/\s+/', trim( $search ), -1, PREG_SPLIT_NO_EMPTY );
492 + return array() !== $terms;
535 493 }
536 494
537 495 /**
538 496 * One page of the POS-servable variation collection, with its total.
@@ -605,14 +563,9 @@
605 563 ),
606 564 );
607 565 }
608 566
609 - add_filter( 'posts_groupby', array( $this, 'group_search_results' ), 10, 2 );
610 - try {
611 - $results = $this->get_objects( $query_args );
612 - } finally {
613 - remove_filter( 'posts_groupby', array( $this, 'group_search_results' ), 10 );
614 - }
567 + $results = $this->get_objects( $query_args );
615 568
616 569 $ids = array();
617 570 foreach ( $results['objects'] as $object ) {
618 571 if ( $object instanceof WC_Product_Variation ) {