| @@ -157,10 +157,9 @@ | ||
| 157 | 157 | * |
| 158 | 158 | * WooCommerce maps `search` onto `s`, which searches post_title/content — useless for a |
| 159 | 159 | * variation, whose title is a generated attribute string. The POS searches what a cashier |
| 160 | 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. | |
| 161 | + * barcode field (`Barcode_Field::search_keys()`). Every term must match at least one carrier. | |
| 163 | 162 | * |
| 164 | 163 | * `sku` is left to WooCommerce: its own exact/comma-list handling is what the |
| 165 | 164 | * sku-beats-search precedence rule relies on. |
| 166 | 165 | */ |
| @@ -172,17 +171,23 @@ | ||
| 172 | 171 | } |
| 173 | 172 | if ( '' !== $search && '' === $sku ) { |
| 174 | 173 | unset( $args['s'] ); |
| 175 | 174 | $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 ) { | |
| 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' ); | |
| 178 | 182 | foreach ( Barcode_Field::search_keys() as $key ) { |
| 179 | - $carriers[] = array( | |
| 183 | + $term_carriers[] = array( | |
| 180 | 184 | 'key' => $key, |
| 181 | 185 | 'value' => $term, |
| 182 | 186 | 'compare' => 'LIKE', |
| 183 | 187 | ); |
| 184 | 188 | } |
| 189 | + $carriers[] = $term_carriers; | |
| 185 | 190 | } |
| 186 | 191 | if ( 1 < \count( $carriers ) ) { |
| 187 | 192 | $args['meta_query'] = $this->add_meta_query( $args, $carriers ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query |
| 188 | 193 | } |
| @@ -437,12 +442,20 @@ | ||
| 437 | 442 | return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'sku must not contain more than 100 comma-separated terms', array( 'status' => 400 ) ); |
| 438 | 443 | } |
| 439 | 444 | } else { |
| 440 | 445 | $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 ) ); | |
| 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 ) ); | |
| 443 | 450 | } |
| 444 | - $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 | + } | |
| 445 | 458 | if ( self::MAX_SEARCH_TERMS < \count( $terms ) ) { |
| 446 | 459 | return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'search must not contain more than 10 whitespace-separated terms', array( 'status' => 400 ) ); |
| 447 | 460 | } |
| 448 | 461 | } |
| @@ -489,8 +502,9 @@ | ||
| 489 | 502 | * code. 1.9.x extended the same enum for the same reason. |
| 490 | 503 | */ |
| 491 | 504 | public function get_collection_params() { |
| 492 | 505 | $params = parent::get_collection_params(); |
| 506 | + $params['search']['sanitize_callback'] = 'rest_sanitize_request_arg'; | |
| 493 | 507 | |
| 494 | 508 | if ( isset( $params['orderby']['enum'] ) && \is_array( $params['orderby']['enum'] ) ) { |
| 495 | 509 | $params['orderby']['enum'] = array_values( |
| 496 | 510 | array_unique( |
| @@ -529,10 +543,14 @@ | ||
| 529 | 543 | return true; |
| 530 | 544 | } |
| 531 | 545 | |
| 532 | 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 | + } | |
| 533 | 551 | |
| 534 | - return array() !== (array) preg_split( '/\s+/', trim( $search ), -1, PREG_SPLIT_NO_EMPTY ); | |
| 552 | + return array() !== $terms; | |
| 535 | 553 | } |
| 536 | 554 | |
| 537 | 555 | /** |
| 538 | 556 | * One page of the POS-servable variation collection, with its total. |