PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.14
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.14
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
woocommerce-pos / includes / API / Customers_Controller.php

Customers_Controller.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.9.14, at includes/API/Customers_Controller.php

755 lines 24.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Customers_Controller.
4 *
5 * @package WCPOS\WooCommercePOS
6 */
7
8 namespace WCPOS\WooCommercePOS\API;
9
10 \defined( 'ABSPATH' ) || die;
11
12 if ( ! class_exists( 'WC_REST_Customers_Controller' ) ) {
13 return;
14 }
15
16 use Exception;
17 use WC_Customer;
18 use WC_REST_Customers_Controller;
19 use WCPOS\WooCommercePOS\Logger;
20 use WCPOS\WooCommercePOS\Services\Tax_Id_Reader;
21 use WCPOS\WooCommercePOS\Services\Tax_Id_Types;
22 use WCPOS\WooCommercePOS\Services\Tax_Id_Writer;
23 use WP_Error;
24 use WP_REST_Request;
25 use WP_REST_Response;
26 use WP_User;
27 use WP_User_Query;
28
29 /**
30 * Product Tgas controller class.
31 *
32 * @NOTE: methods not prefixed with wcpos_ will override WC_REST_Customers_Controller methods
33 */
34 class Customers_Controller extends WC_REST_Customers_Controller {
35 use Traits\Query_Helpers;
36 use Traits\Uuid_Handler;
37 use Traits\WCPOS_REST_API;
38
39 /**
40 * Endpoint namespace.
41 *
42 * @var string
43 */
44 protected $namespace = 'wcpos/v1';
45
46 /**
47 * Store user search results for merging with meta_query search results.
48 *
49 * @var array
50 */
51 protected $wcpos_user_search_results = array();
52
53 /**
54 * Store the request object for use in lifecycle methods.
55 *
56 * @var WP_REST_Request
57 */
58 protected $wcpos_request;
59
60 /**
61 * Dispatch request to parent controller, or override if needed.
62 *
63 * @param mixed $dispatch_result Dispatch result, will be used if not empty.
64 * @param WP_REST_Request $request Request used to generate the response.
65 * @param string $route Route matched for the request.
66 * @param array $handler Route handler used for the request.
67 */
68 public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $request, $route, $handler ) {
69 $this->wcpos_request = $request;
70
71 add_filter( 'woocommerce_rest_prepare_customer', array( $this, 'wcpos_customer_response' ), 10, 3 );
72 add_filter( 'woocommerce_rest_customer_query', array( $this, 'wcpos_customer_query' ), 10, 2 );
73 add_filter( 'is_protected_meta', array( $this, 'wcpos_allow_uuid_meta' ), 10, 3 );
74
75 /*
76 * Check if the request is for all customers and if the 'posts_per_page' is set to -1.
77 * Optimised query for getting all customer IDs.
78 */
79 if ( -1 == $request->get_param( 'posts_per_page' ) && null !== $request->get_param( 'fields' ) ) {
80 return $this->wcpos_get_all_posts( $request );
81 }
82
83 return $dispatch_result;
84 }
85
86 /**
87 * Add custom fields to the product schema.
88 */
89 public function get_item_schema() {
90 $schema = parent::get_item_schema();
91
92 // Check and remove email format validation from the billing property.
93 if ( isset( $schema['properties']['billing']['properties']['email']['format'] ) ) {
94 unset( $schema['properties']['billing']['properties']['email']['format'] );
95 }
96
97 // Add structured tax_ids property (TaxId[]).
98 $schema['properties']['tax_ids'] = array(
99 'description' => /* translators: REST API schema field label or error message. */ __( 'Customer tax IDs.', 'woocommerce-pos' ),
100 'type' => 'array',
101 'context' => array( 'view', 'edit' ),
102 'items' => array(
103 'type' => 'object',
104 'properties' => array(
105 'type' => array(
106 'type' => 'string',
107 'enum' => Tax_Id_Types::all_types(),
108 'description' => /* translators: REST API schema field label or error message. */ __( 'Tax ID type.', 'woocommerce-pos' ),
109 ),
110 'value' => array(
111 'type' => 'string',
112 'description' => /* translators: REST API schema field label or error message. */ __( 'Tax ID value.', 'woocommerce-pos' ),
113 ),
114 'country' => array(
115 'type' => array( 'string', 'null' ),
116 'description' => __( 'ISO 3166-1 alpha-2 country code.', 'woocommerce-pos' ),
117 ),
118 'label' => array(
119 'type' => array( 'string', 'null' ),
120 'description' => /* translators: REST API schema field label or error message. */ __( 'Optional human-readable label.', 'woocommerce-pos' ),
121 ),
122 ),
123 ),
124 );
125
126 return $schema;
127 }
128
129 /**
130 * Check if a given request has access to create a customer.
131 *
132 * WC checks promote_users (< 9.9) or create_customers (9.9+). The POS
133 * fallback checks only the version-appropriate capability so it matches
134 * the toggle shown on the Access settings page.
135 *
136 * @param WP_REST_Request $request Full details about the request.
137 *
138 * @return WP_Error|bool
139 */
140 public function create_item_permissions_check( $request ) {
141 $permission = parent::create_item_permissions_check( $request );
142
143 if ( is_wp_error( $permission ) ) {
144 $customer_create_cap = version_compare( WC()->version, '9.9', '>=' )
145 ? 'create_customers'
146 : 'promote_users';
147
148 if ( current_user_can( $customer_create_cap ) ) {
149 return true;
150 }
151 }
152
153 return $permission;
154 }
155
156 /**
157 * Check if a given request has access to update a customer.
158 *
159 * WC checks edit_users. The POS fallback also checks edit_users so the
160 * Access settings page toggle controls this behaviour.
161 *
162 * @param WP_REST_Request $request Full details about the request.
163 *
164 * @return WP_Error|bool
165 */
166 public function update_item_permissions_check( $request ) {
167 $permission = parent::update_item_permissions_check( $request );
168
169 if ( is_wp_error( $permission ) && current_user_can( 'edit_users' ) ) {
170 return true;
171 }
172
173 return $permission;
174 }
175
176 /**
177 * Add extra fields to WP_REST_Controller::get_collection_params().
178 * - add new fields to the 'orderby' enum list.
179 */
180 public function get_collection_params() {
181 $params = parent::get_collection_params();
182
183 // Check if 'orderby' is set and is an array before modifying it.
184 if ( isset( $params['orderby'] ) && \is_array( $params['orderby']['enum'] ) ) {
185 // Add new fields to the 'orderby' enum list.
186 $new_orderby_options = array(
187 'first_name',
188 'last_name',
189 'email',
190 'role',
191 'username',
192 );
193 foreach ( $new_orderby_options as $option ) {
194 if ( ! \in_array( $option, $params['orderby']['enum'], true ) ) {
195 $params['orderby']['enum'][] = $option;
196 }
197 }
198 }
199
200 // Add 'roles' filter, this allows us to filter by multiple roles.
201 $params['roles'] = array(
202 'description' => __( 'Filter customers by roles.', 'woocommerce-pos' ),
203 'type' => 'array',
204 'items' => array(
205 'type' => 'string',
206 ),
207 'required' => false,
208 );
209
210 return $params;
211 }
212
213 /**
214 * Create a single item.
215 *
216 * @param WP_REST_Request $request Full details about the request.
217 *
218 * @return WP_Error|WP_REST_Response
219 */
220 public function create_item( $request ) {
221 $valid_email = $this->wcpos_validate_billing_email( $request );
222 if ( is_wp_error( $valid_email ) ) {
223 return $valid_email;
224 }
225
226 /*
227 * Generate a password for the new user.
228 * Add filter for get_option key 'woocommerce_registration_generate_password' to ensure it is set to 'yes'.
229 */
230 add_filter(
231 'pre_option_woocommerce_registration_generate_password',
232 function () {
233 return 'yes';
234 }
235 );
236
237 /*
238 * Optionally generate a username from the customer's email address.
239 * Reads the POS 'generate_username' general setting and overrides the
240 * store-level 'woocommerce_registration_generate_username' option so the
241 * POS behaviour is independent of the online-checkout setting.
242 */
243 $generate_username = wcpos_get_settings( 'general', 'generate_username' );
244 add_filter(
245 'pre_option_woocommerce_registration_generate_username',
246 function () use ( $generate_username ) {
247 return $generate_username ? 'yes' : 'no';
248 }
249 );
250
251 // Proceed with the parent method to handle the creation.
252 $response = parent::create_item( $request );
253 $this->wcpos_persist_tax_ids_from_request( $response, $request );
254
255 return $response;
256 }
257
258 /**
259 * Update a single order.
260 *
261 * @param WP_REST_Request $request Full details about the request.
262 *
263 * @return WP_Error|WP_REST_Response
264 */
265 public function update_item( $request ) {
266 $valid_email = $this->wcpos_validate_billing_email( $request );
267 if ( is_wp_error( $valid_email ) ) {
268 return $valid_email;
269 }
270
271 // Proceed with the parent method to handle the creation.
272 $response = parent::update_item( $request );
273 $this->wcpos_persist_tax_ids_from_request( $response, $request );
274
275 return $response;
276 }
277
278 /**
279 * Persist `tax_ids` from a create/update request via Tax_Id_Writer.
280 *
281 * No-op when the request did not include `tax_ids`, the response is an
282 * error, or the resolved user ID is invalid.
283 *
284 * @param mixed $response Response from the parent controller.
285 * @param WP_REST_Request $request Original request.
286 */
287 protected function wcpos_persist_tax_ids_from_request( $response, WP_REST_Request $request ): void {
288 if ( ! ( $response instanceof WP_REST_Response ) ) {
289 return;
290 }
291 $tax_ids = $request->get_param( 'tax_ids' );
292 if ( ! \is_array( $tax_ids ) ) {
293 return;
294 }
295
296 $data = $response->get_data();
297 $user_id = isset( $data['id'] ) ? (int) $data['id'] : 0;
298 if ( $user_id <= 0 ) {
299 return;
300 }
301
302 ( new Tax_Id_Writer() )->write_for_user( $user_id, $tax_ids );
303
304 // Reflect persisted list in the response.
305 $data['tax_ids'] = ( new Tax_Id_Reader() )->read_for_user( $user_id );
306 $response->set_data( $data );
307 }
308
309 /**
310 * Validate billing email.
311 * NOTE: we have removed the format check to allow empty email addresses.
312 *
313 * @param WP_REST_Request $request The REST request object.
314 *
315 * @return bool|WP_Error
316 */
317 public function wcpos_validate_billing_email( WP_REST_Request $request ) {
318 // Your custom validation logic for the request data.
319 $billing = $request['billing'] ?? null;
320 $email = \is_array( $billing ) ? ( $billing['email'] ?? null ) : null;
321
322 if ( ! \is_null( $email ) && '' !== $email && ! is_email( $email ) ) {
323 return new WP_Error(
324 'rest_invalid_param',
325 // translators: Use default WordPress translation.
326 __( 'Invalid email address.', 'woocommerce-pos' ),
327 array( 'status' => 400 )
328 );
329 }
330
331 return true;
332 }
333
334 /**
335 * Filter customer data returned from the REST API.
336 *
337 * @param WP_REST_Response $response The response object.
338 * @param WP_User $user_data User object used to create response.
339 * @param WP_REST_Request $request Request object.
340 */
341 public function wcpos_customer_response( WP_REST_Response $response, WP_User $user_data, WP_REST_Request $request ): WP_REST_Response {
342 $data = $response->get_data();
343
344 // Add the uuid to the response.
345 $this->maybe_add_user_uuid( $user_data );
346
347 /*
348 * Add the customer meta data to the response
349 *
350 * In the WC REST Customers Controller -> get_formatted_item_data_core function, the customer's
351 * meta_data is only added for administrators. I assume this is for privacy/security reasons?
352 *
353 * Even for administrators, meta data starting with '_' will be filtered out.
354 * We need to add the uuid meta_data to the response for all cashiers and also non-protected meta.
355 *
356 * This means we let of junk meta_data into the response, but at least we don't block data and allow
357 * saving of meta_data.
358 *
359 * @TODO - add filter settings to block/allow meta_data keys
360 */
361 try {
362 $customer = new WC_Customer( $user_data->ID );
363 $raw_meta_data = $customer->get_meta_data();
364
365 // Monitor meta count.
366 $this->wcpos_monitor_meta_count( $customer, $raw_meta_data );
367
368 $filtered_meta_data = array_filter(
369 $raw_meta_data,
370 function ( $meta ) {
371 return ! is_protected_meta( $meta->key, 'user' );
372 }
373 );
374
375 // Convert to WC REST API expected format.
376 $data['meta_data'] = array_map(
377 function ( $meta ) {
378 return array(
379 'id' => $meta->id,
380 'key' => $meta->key,
381 'value' => $meta->value,
382 );
383 },
384 array_values( $filtered_meta_data )
385 );
386 } catch ( Exception $e ) {
387 Logger::log( 'Error getting customer meta data: ' . $e->getMessage() );
388 }
389
390 // Add structured tax_ids list (read fallback across legacy plugin meta keys).
391 $data['tax_ids'] = ( new Tax_Id_Reader() )->read_for_user( $user_data->ID );
392
393 // Estimate response size and log if excessive.
394 $this->wcpos_estimate_response_size( $data, $user_data->ID, 'Customer' );
395
396 // Set any changes to the response data.
397 $response->set_data( $data );
398
399 return $response;
400 }
401
402 /**
403 * Returns array of all customer ids.
404 *
405 * Note: user queries are a little more complicated than post queries, for example,
406 * multisite would return all users from all sites, not just the current site.
407 * Also, querying by role is not as simple as querying by post type.
408 *
409 * @param WP_REST_Request $request Full details about the request.
410 *
411 * @return WP_Error|WP_REST_Response
412 */
413 public function wcpos_get_all_posts( $request ) {
414 global $wpdb;
415
416 // Start timing execution.
417 $start_time = microtime( true );
418
419 $modified_after = $request->get_param( 'modified_after' );
420 $dates_are_gmt = true;
421 $fields = $request->get_param( 'fields' );
422 $id_with_modified_date = array( 'id', 'date_modified_gmt' ) === $fields;
423
424 $args = array(
425 'fields' => array( 'ID', 'user_registered' ), // Return only the ID and registered date.
426 // 'role__in' => 'all', // @TODO: could be an array of roles, like ['customer', 'cashier'].
427 );
428
429 /*
430 * The user query is too complex to do a direct sql query, eg: multisite would return all users from all sites,
431 * not just the current site. Also, querying by role is not as simple as querying by post type.
432 *
433 * For now we get all user ids and all 'last_update' meta values, then combine them into an array of objects.
434 */
435 try {
436 $user_query = new WP_User_Query( $args );
437 $users = $user_query->get_results();
438 $last_updates = array();
439
440 if ( $id_with_modified_date ) {
441 $query = "
442 SELECT user_id, meta_value
443 FROM $wpdb->usermeta
444 WHERE meta_key = 'last_update'
445 ";
446
447 // If modified_after param is set, add the condition to the query.
448 if ( $modified_after ) {
449 $modified_after_timestamp = strtotime( $modified_after );
450 $query .= $wpdb->prepare( ' AND meta_value > %d', $modified_after_timestamp );
451 }
452
453 $last_update_results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared conditionally above.
454
455 // Manually create the associative array of user_id => last_update.
456 foreach ( $last_update_results as $result ) {
457 $last_updates[ $result->user_id ] = is_numeric( $result->meta_value ) ? gmdate( 'Y-m-d\TH:i:s', (int) $result->meta_value ) : null;
458 }
459 }
460
461 /**
462 * Performance notes:
463 * - Using a generator is faster than array_map when dealing with large datasets.
464 * - If date is in the format 'Y-m-d H:i:s' we just do preg_replace to 'Y-m-d\TH:i:s',
465 * rather than using wc_rest_prepare_date_response
466 *
467 * This resulted in execution time of 10% of the original time.
468 *
469 * If the modified_after param is set, we don't need to loop through the entire user list.
470 * The last_update_results array will only contain the users that have been modified after the given date.
471 * We just need to check they are valid user ids, this sucks, but there could be orphaned last_update meta values.
472 */
473 $formatted_results = array();
474
475 if ( $modified_after ) {
476 foreach ( $users as $user ) {
477 if ( isset( $last_updates[ $user->ID ] ) ) {
478 $user_info = array( 'id' => (int) $user->ID );
479 if ( $id_with_modified_date ) {
480 $user_info['date_modified_gmt'] = $last_updates[ $user->ID ];
481 }
482 $formatted_results[] = $user_info;
483 }
484 }
485 } else {
486 $formatted_results = iterator_to_array(
487 ( function () use ( $users, $last_updates, $id_with_modified_date ) {
488 foreach ( $users as $user ) {
489 $user_info = array( 'id' => (int) $user->ID );
490 if ( $id_with_modified_date ) {
491 if ( isset( $last_updates[ $user->ID ] ) && ! empty( $last_updates[ $user->ID ] ) ) {
492 $user_info['date_modified_gmt'] = $last_updates[ $user->ID ];
493 } else {
494 $user_info['date_modified_gmt'] = null; // users can have null date_modified_gmt.
495 }
496 }
497 yield $user_info;
498 }
499 } )()
500 );
501 }
502
503 // Get the total number of orders for the given criteria.
504 $total = \count( $formatted_results );
505
506 // Collect execution time and server load.
507 $execution_time = microtime( true ) - $start_time;
508 $execution_time_ms = number_format( $execution_time * 1000, 2 );
509 $server_load = $this->get_server_load();
510
511 $response = rest_ensure_response( $formatted_results );
512 $response->header( 'X-WP-Total', (string) $total );
513 $response->header( 'X-Execution-Time', $execution_time_ms . ' ms' );
514 $response->header( 'X-Server-Load', json_encode( $server_load ) );
515
516 return $response;
517 } catch ( Exception $e ) {
518 Logger::log( 'Error fetching order IDs: ' . $e->getMessage() );
519
520 return new WP_Error(
521 'woocommerce_pos_rest_cannot_fetch',
522 'Error fetching customer IDs.',
523 array( 'status' => 500 )
524 );
525 }
526 }
527
528 /**
529 * Filter arguments, before passing to WP_User_Query, when querying users via the REST API.
530 *
531 * @param array $prepared_args Array of arguments for WP_User_Query.
532 * @param WP_REST_Request $request The current request.
533 *
534 * @return array $prepared_args Array of arguments for WP_User_Query.
535 */
536 public function wcpos_customer_query( array $prepared_args, WP_REST_Request $request ): array {
537 $query_params = $request->get_query_params();
538
539 // add modified_after date_modified_gmt.
540 if ( isset( $query_params['modified_after'] ) && '' !== $query_params['modified_after'] ) {
541 $timestamp = strtotime( $query_params['modified_after'] );
542 $prepared_args['meta_query'] = $this->wcpos_combine_meta_queries(
543 array(
544 array(
545 'key' => 'last_update',
546 'value' => $timestamp ? (string) $timestamp : '',
547 'compare' => '>',
548 ),
549 ),
550 $prepared_args['meta_query']
551 );
552 }
553
554 // Handle orderby cases.
555 if ( isset( $query_params['orderby'] ) ) {
556 switch ( $query_params['orderby'] ) {
557 case 'first_name':
558 $prepared_args['meta_key'] = 'first_name';
559 $prepared_args['orderby'] = 'meta_value';
560
561 break;
562
563 case 'last_name':
564 $prepared_args['meta_key'] = 'last_name';
565 $prepared_args['orderby'] = 'meta_value';
566
567 break;
568
569 case 'email':
570 $prepared_args['orderby'] = 'user_email';
571
572 break;
573
574 case 'role':
575 $prepared_args['meta_key'] = 'wp_capabilities';
576 $prepared_args['orderby'] = 'meta_value';
577
578 break;
579
580 case 'username':
581 $prepared_args['orderby'] = 'user_login';
582
583 break;
584
585 default:
586 break;
587 }
588 }
589
590 // Handle search.
591 if ( isset( $query_params['search'] ) && ! empty( $query_params['search'] ) ) {
592 $search_keyword = $query_params['search'];
593
594 /*
595 * It seems that you can't search by user_email, user_login etc and meta_query at the same time.
596 *
597 * We will unset the search param and add a hook to modify the user query to search the user table
598 */
599 unset( $prepared_args['search'] );
600 $prepared_args['_wcpos_search'] = $search_keyword; // store the search keyword for later use.
601 add_action( 'pre_user_query', array( $this, 'wcpos_search_user_table' ) );
602
603 $search_meta_query = array(
604 'relation' => 'OR',
605 array(
606 'key' => 'first_name',
607 'value' => $search_keyword,
608 'compare' => 'LIKE',
609 ),
610 array(
611 'key' => 'last_name',
612 'value' => $search_keyword,
613 'compare' => 'LIKE',
614 ),
615 // WooCommerce billing fields.
616 array(
617 'key' => 'billing_first_name',
618 'value' => $search_keyword,
619 'compare' => 'LIKE',
620 ),
621 array(
622 'key' => 'billing_last_name',
623 'value' => $search_keyword,
624 'compare' => 'LIKE',
625 ),
626 array(
627 'key' => 'billing_email',
628 'value' => $search_keyword,
629 'compare' => 'LIKE',
630 ),
631 array(
632 'key' => 'billing_company',
633 'value' => $search_keyword,
634 'compare' => 'LIKE',
635 ),
636 array(
637 'key' => 'billing_phone',
638 'value' => $search_keyword,
639 'compare' => 'LIKE',
640 ),
641 );
642
643 foreach ( Tax_Id_Reader::fallback_user_meta_keys() as $meta_key ) {
644 $search_meta_query[] = array(
645 'key' => $meta_key,
646 'value' => $search_keyword,
647 'compare' => 'LIKE',
648 );
649 }
650
651 // Combine the search meta_query with the existing meta_query.
652 $prepared_args['meta_query'] = $this->wcpos_combine_meta_queries( $search_meta_query, $prepared_args['meta_query'] );
653 }
654
655 // Handle include/exclude.
656 if ( isset( $request['wcpos_include'] ) || isset( $request['wcpos_exclude'] ) ) {
657 add_action( 'pre_user_query', array( $this, 'wcpos_include_exclude_users_by_id' ) );
658 }
659
660 // Filter by roles (this is a comma separated list of roles).
661 if ( ! empty( $request['roles'] ) && \is_array( $request['roles'] ) ) {
662 $roles = array_map( 'sanitize_text_field', $request['roles'] );
663 $prepared_args['role__in'] = $roles;
664 // remove $prepared_args['role'] to prevent it from overriding $prepared_args['role__in'].
665 unset( $prepared_args['role'] );
666 }
667
668 return $prepared_args;
669 }
670
671 /**
672 * Add user_email and user_login to the user query.
673 *
674 * @param WP_User_Query $query The WP_User_Query instance (passed by reference).
675 */
676 public function wcpos_search_user_table( $query ): void {
677 global $wpdb;
678
679 // Remove the hook.
680 remove_action( 'pre_user_query', array( $this, 'wcpos_search_user_table' ) );
681
682 // Get the search keyword.
683 $query_params = $query->query_vars;
684 $search_keyword = $query_params['_wcpos_search'];
685
686 // Prepare the LIKE statement.
687 $like_email = '%' . $wpdb->esc_like( $search_keyword ) . '%';
688 $like_login = '%' . $wpdb->esc_like( $search_keyword ) . '%';
689
690 $insertion = $wpdb->prepare(
691 "({$wpdb->users}.user_email LIKE %s) OR ({$wpdb->users}.user_login LIKE %s) OR ",
692 $like_email,
693 $like_login
694 );
695
696 $pattern = "/\(\s*\w+\.meta_key\s*=\s*'[^']+'\s*AND\s*\w+\.meta_value\s*LIKE\s*'[^']+'\s*\)(\s*OR\s*\(\s*\w+\.meta_key\s*=\s*'[^']+'\s*AND\s*\w+\.meta_value\s*LIKE\s*'[^']+'\s*\))*\s*/";
697
698 // Add the search keyword to the query.
699 $modified_where = preg_replace( $pattern, "$insertion$0", $query->query_where );
700
701 // Check if the replacement was successful and assign it back to query_where.
702 if ( $modified_where !== $query->query_where ) {
703 $query->query_where = $modified_where;
704 }
705 }
706
707 /**
708 * Include or exclude users by ID.
709 *
710 * @param WP_User_Query $query The WP_User_Query instance (passed by reference).
711 */
712 public function wcpos_include_exclude_users_by_id( $query ): void {
713 global $wpdb;
714
715 // Remove the hook.
716 remove_action( 'pre_user_query', array( $this, 'wcpos_include_exclude_users_by_id' ) );
717
718 // Handle 'wcpos_include'.
719 if ( ! empty( $this->wcpos_request['wcpos_include'] ) ) {
720 $include_ids = array_map( 'intval', (array) $this->wcpos_request['wcpos_include'] );
721 $ids_format = implode( ',', array_fill( 0, \count( $include_ids ), '%d' ) );
722 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $ids_format is a safe placeholder string.
723 $query->query_where .= $wpdb->prepare( " AND {$wpdb->users}.ID IN ($ids_format) ", $include_ids );
724 }
725
726 // Handle 'wcpos_exclude'.
727 if ( ! empty( $this->wcpos_request['wcpos_exclude'] ) ) {
728 $exclude_ids = array_map( 'intval', (array) $this->wcpos_request['wcpos_exclude'] );
729 $ids_format = implode( ',', array_fill( 0, \count( $exclude_ids ), '%d' ) );
730 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $ids_format is a safe placeholder string.
731 $query->query_where .= $wpdb->prepare( " AND {$wpdb->users}.ID NOT IN ($ids_format) ", $exclude_ids );
732 }
733 }
734
735 /**
736 * Allow the _woocommerce_pos_uuid meta key to be saved via the REST API.
737 *
738 * By default, meta keys starting with '_' are protected and cannot be set via the REST API.
739 * We need to allow our UUID meta key so that UUIDs sent from the POS app are preserved.
740 *
741 * @param bool $protected Whether the meta key is considered protected.
742 * @param string $meta_key The meta key being checked.
743 * @param string $meta_type The type of object the meta is for (post, user, etc.).
744 *
745 * @return bool Whether the meta key should be protected.
746 */
747 public function wcpos_allow_uuid_meta( $protected, $meta_key, $meta_type ) {
748 if ( '_woocommerce_pos_uuid' === $meta_key && 'user' === $meta_type ) {
749 return false;
750 }
751
752 return $protected;
753 }
754 }
755