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.27 1.10.26 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 All 171 releases
← All changes | includes/API/V2/Variations_Controller.php +85 -37 1.10.3 → 1.10.17 View file →
@@ -10,8 +10,10 @@
10 10 use WC_Product_Variation;
11 11 use WC_REST_Product_Variations_Controller;
12 12 use WCPOS\WooCommercePOS\Services\Barcode_Field;
13 13 use WCPOS\WooCommercePOS\Sync\Api;
14 +use WCPOS\WooCommercePOS\Sync\Collection_Rules;
15 +use WCPOS\WooCommercePOS\Sync\Collection_Rules_Plan;
14 16 use WCPOS\WooCommercePOS\Sync\Digest_Index;
15 17 use WCPOS\WooCommercePOS\Sync\Endpoint_Permissions;
16 18 use WCPOS\WooCommercePOS\Sync\Pos_Visibility;
17 19 use WCPOS\WooCommercePOS\Sync\Product_Serializer;
@@ -49,8 +51,25 @@
49 51 * (`documents[].{id,parent_id,payload,_rxdb_digest}`), POS visibility, the barcode carrier
50 52 * search, and the request bounds. Everything else is WooCommerce's.
51 53 */
52 54 class Variations_Controller extends WC_REST_Product_Variations_Controller {
55 + /**
56 + * Request keys the variation Collection Rules plan reads on this lane.
57 + *
58 + * @var array
59 + */
60 + private const WCPOS_SORT_PARAM_MAP = array(
61 + 'orderby' => 'orderby',
62 + 'order' => 'order',
63 + );
64 +
65 + /**
66 + * The request whose declared sort `wcpos_posts_clauses()` applies.
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;
@@ -138,10 +157,9 @@
138 157 *
139 158 * WooCommerce maps `search` onto `s`, which searches post_title/content — useless for a
140 159 * variation, whose title is a generated attribute string. The POS searches what a cashier
141 160 * 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.
161 + * barcode field (`Barcode_Field::search_keys()`). Every term must match at least one carrier.
144 162 *
145 163 * `sku` is left to WooCommerce: its own exact/comma-list handling is what the
146 164 * sku-beats-search precedence rule relies on.
147 165 */
@@ -153,17 +171,23 @@
153 171 }
154 172 if ( '' !== $search && '' === $sku ) {
155 173 unset( $args['s'] );
156 174 $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 ) {
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' );
159 182 foreach ( Barcode_Field::search_keys() as $key ) {
160 - $carriers[] = array(
183 + $term_carriers[] = array(
161 184 'key' => $key,
162 185 'value' => $term,
163 186 'compare' => 'LIKE',
164 187 );
165 188 }
189 + $carriers[] = $term_carriers;
166 190 }
167 191 if ( 1 < \count( $carriers ) ) {
168 192 $args['meta_query'] = $this->add_meta_query( $args, $carriers ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
169 193 }
@@ -203,37 +227,21 @@
203 227 */
204 228 $args = ( new Pos_Visibility() )->apply_to_wp_query_args( $args, Pos_Visibility::VARIATIONS );
205 229
206 230 /*
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.
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.
210 240 */
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';
241 + $this->wcpos_sort_request = $request;
242 + add_filter( 'posts_clauses', array( $this, 'wcpos_posts_clauses' ), 10, 2 );
216 243
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;
233 - }
234 - }
235 -
236 244 return $args;
237 245 }
238 246
239 247 /**
@@ -434,12 +442,20 @@
434 442 return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'sku must not contain more than 100 comma-separated terms', array( 'status' => 400 ) );
435 443 }
436 444 } else {
437 445 $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 ) );
446 + // Unlike mb_strlen(), PCRE is independent of blog_charset and detects malformed UTF-8.
447 + $characters = preg_match_all( '/./us', $search );
448 + if ( false === $characters ) {
449 + return new WP_Error( 'woocommerce_pos_variations_search_invalid', 'search must be valid UTF-8', array( 'status' => 400 ) );
440 450 }
441 - $terms = (array) preg_split( '/\s+/', trim( $search ), -1, PREG_SPLIT_NO_EMPTY );
451 + if ( self::MAX_SEARCH_LENGTH < $characters ) {
452 + return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'search must not exceed 256 characters', array( 'status' => 400 ) );
453 + }
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 + }
442 458 if ( self::MAX_SEARCH_TERMS < \count( $terms ) ) {
443 459 return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'search must not contain more than 10 whitespace-separated terms', array( 'status' => 400 ) );
444 460 }
445 461 }
@@ -451,8 +467,35 @@
451 467 return true;
452 468 }
453 469
454 470 /**
471 + * Apply the declared POS variation sorts to the SQL clauses.
472 + *
473 + * `posts_clauses` fires for EVERY WP_Query, so the body is guarded by post type and by
474 + * the plan itself — it contributes nothing unless this request claimed one of the
475 + * declared sorts.
476 + *
477 + * @param array $clauses Associative array of the clauses for the query.
478 + * @param WP_Query $wp_query The WP_Query instance.
479 + *
480 + * @return array
481 + */
482 + public function wcpos_posts_clauses( array $clauses, WP_Query $wp_query ): array {
483 + if ( null === $this->wcpos_sort_request ) {
484 + return $clauses;
485 + }
486 +
487 + $post_type = $wp_query->query_vars['post_type'] ?? null;
488 + if ( 'product_variation' !== $post_type && ( ! \is_array( $post_type ) || ! \in_array( 'product_variation', $post_type, true ) ) ) {
489 + return $clauses;
490 + }
491 +
492 + $plan = Collection_Rules::for_request( 'variations', $this->wcpos_sort_request, self::WCPOS_SORT_PARAM_MAP );
493 +
494 + return $plan->filter( Collection_Rules_Plan::HOOK_POSTS_CLAUSES, $clauses, $wp_query );
495 + }
496 +
497 + /**
455 498 * WooCommerce's collection params, plus the sort keys the POS grids offer.
456 499 *
457 500 * `orderby` is a validated enum. Appending here is what lets `prepare_objects_query()` act on
458 501 * these four — otherwise the request 400s during argument validation and the switch is dead
@@ -459,8 +502,9 @@
459 502 * code. 1.9.x extended the same enum for the same reason.
460 503 */
461 504 public function get_collection_params() {
462 505 $params = parent::get_collection_params();
506 + $params['search']['sanitize_callback'] = 'rest_sanitize_request_arg';
463 507
464 508 if ( isset( $params['orderby']['enum'] ) && \is_array( $params['orderby']['enum'] ) ) {
465 509 $params['orderby']['enum'] = array_values(
466 510 array_unique(
@@ -465,9 +509,9 @@
465 509 $params['orderby']['enum'] = array_values(
466 510 array_unique(
467 511 array_merge(
468 512 $params['orderby']['enum'],
469 - array( 'sku', 'barcode', 'stock_quantity', 'stock_status' )
513 + Collection_Rules::orderby_enum( 'variations' )
470 514 )
471 515 )
472 516 );
473 517 }
@@ -499,10 +543,14 @@
499 543 return true;
500 544 }
501 545
502 546 $search = (string) ( $request->get_param( '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 + }
503 551
504 - return array() !== (array) preg_split( '/\s+/', trim( $search ), -1, PREG_SPLIT_NO_EMPTY );
552 + return array() !== $terms;
505 553 }
506 554
507 555 /**
508 556 * One page of the POS-servable variation collection, with its total.