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.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 / V2 / Proxy / Customers_Proxy_Behavior.php

Customers_Proxy_Behavior.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.17, at includes/API/V2/Proxy/Customers_Proxy_Behavior.php

255 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Customer catalog proxy behavior.
4 *
5 * @package WCPOS\WooCommercePOS\API\V2\Proxy
6 */
7
8 namespace WCPOS\WooCommercePOS\API\V2\Proxy;
9
10 use WCPOS\WooCommercePOS\Services\Tax_Id_Reader;
11 use WCPOS\WooCommercePOS\Sync\Collection_Rules;
12 use WP_REST_Request;
13 use WP_User_Query;
14
15 /**
16 * Applies the customer search, filters and extended sorts without a v1 controller.
17 */
18 final class Customers_Proxy_Behavior extends Scoped_Proxy_Behavior {
19 /**
20 * Params this behavior claims and handles itself, rather than forwarding.
21 *
22 * @var array
23 */
24 private $delegated = array();
25
26 /**
27 * Claim the customer params wc/v3 cannot express, and default the role filter.
28 *
29 * @param array $params Query parameters to forward.
30 * @param WP_REST_Request $request Original proxy request.
31 *
32 * @return array
33 */
34 public function forwarded_params( array $params, WP_REST_Request $request ): array {
35 $this->delegated = array();
36 if ( ! isset( $params['role'] ) ) {
37 $params['role'] = 'all';
38 }
39 if ( isset( $params['search'] ) ) {
40 $search = trim( (string) $params['search'] );
41 unset( $params['search'] );
42 if ( 0 !== preg_match( '/\S/u', $search ) ) {
43 $this->delegated['search'] = $search;
44 }
45 }
46 if ( isset( $params['orderby'] ) && \in_array( (string) $params['orderby'], Collection_Rules::orderby_enum( 'customers' ), true ) ) {
47 $this->delegated['orderby'] = (string) $params['orderby'];
48 unset( $params['orderby'] );
49 }
50
51 /*
52 * `roles` (plural) is WCPOS's multi-role filter, and `modified_after` is how the
53 * client asks for the customers touched since its last pull. Neither exists in
54 * wc/v3, which drops an unregistered param in silence — so before this claim the
55 * proxy lane answered a narrowed request with the UNNARROWED list, and a
56 * `modified_after` sync pull re-fetched the whole customer space every tick.
57 * `wcpos/v1` is the frozen authority for both, so the reads below reproduce its
58 * gates verbatim (`roles` must be a non-empty array; a blank date is no filter).
59 */
60 $roles = $params['roles'] ?? null;
61 if ( null !== $roles && ! \is_array( $roles ) ) {
62 // v1 gets this for free: its `roles` schema row is `type => array`, and WP's
63 // own arg sanitizer runs `wp_parse_list()` on a comma-joined string. The proxy
64 // route carries no schema, so a `roles=a,b` request would otherwise be an
65 // array on one lane and an ignored string on the other.
66 $roles = wp_parse_list( $roles );
67 }
68 if ( ! empty( $roles ) ) {
69 $this->delegated['roles'] = array_map( 'sanitize_text_field', $roles );
70 }
71 unset( $params['roles'] );
72 if ( isset( $params['modified_after'] ) && '' !== $params['modified_after'] ) {
73 $this->delegated['modified_after'] = (string) $params['modified_after'];
74 }
75 unset( $params['modified_after'] );
76
77 return $params;
78 }
79
80 /**
81 * Install this resource's hooks and return their removal tuples.
82 *
83 * @return array<int, array{0: string, 1: callable, 2: int}>
84 */
85 protected function install(): array {
86 if ( array() === $this->delegated ) {
87 return array();
88 }
89
90 $bindings = array();
91 add_filter( 'woocommerce_rest_customer_query', array( $this, 'customer_query' ) );
92 $bindings[] = array( 'woocommerce_rest_customer_query', array( $this, 'customer_query' ), 10 );
93 if ( isset( $this->delegated['search'] ) ) {
94 add_action( 'pre_user_query', array( $this, 'search_user_table' ) );
95 $bindings[] = array( 'pre_user_query', array( $this, 'search_user_table' ), 10 );
96 }
97 if ( 'role' === ( $this->delegated['orderby'] ?? null ) ) {
98 add_action( 'pre_user_query', array( $this, 'orderby_role' ) );
99 $bindings[] = array( 'pre_user_query', array( $this, 'orderby_role' ), 10 );
100 }
101
102 return $bindings;
103 }
104
105 /**
106 * Apply the delegated customer parameters to WP_User_Query arguments.
107 *
108 * @param array $args Prepared arguments.
109 *
110 * @return array
111 */
112 public function customer_query( array $args ): array {
113 switch ( $this->delegated['orderby'] ?? '' ) {
114 case 'first_name':
115 case 'last_name':
116 $args['meta_key'] = $this->delegated['orderby'];
117 $args['orderby'] = 'meta_value';
118 break;
119 case 'email':
120 $args['orderby'] = 'user_email';
121 break;
122 case 'role':
123 $args['_wcpos_orderby_role'] = true;
124 break;
125 case 'username':
126 $args['orderby'] = 'user_login';
127 break;
128 }
129 if ( isset( $this->delegated['search'] ) ) {
130 unset( $args['search'] );
131 $args['_wcpos_search'] = $this->delegated['search'];
132 }
133 if ( isset( $this->delegated['modified_after'] ) ) {
134 $timestamp = strtotime( $this->delegated['modified_after'] );
135
136 /*
137 * `last_update` holds a Unix timestamp, but it is stored as usermeta text and
138 * `WP_Meta_Query` defaults an untyped clause to CHAR — which compares it as a
139 * string. Timestamps only sort the same way as strings while they are the same
140 * length, so a cutoff from before 2001-09-09 (nine digits) drops every current
141 * customer, whose timestamp is ten digits starting with a `1`: `'1787465309' >
142 * '946684800'` is false, character by character. NUMERIC casts to SIGNED and
143 * compares the numbers. The same controller's bulk-id fast path already binds
144 * this value with `%d`, so this makes the two agree.
145 */
146 $last_update = array(
147 'key' => 'last_update',
148 'value' => $timestamp ? (string) $timestamp : '',
149 'compare' => '>',
150 'type' => 'NUMERIC',
151 );
152
153 /*
154 * AND our row onto whatever is already there rather than replacing it, so a
155 * third party filtering the same query keeps its clauses. `wcpos/v1` flattens
156 * two AND-related sets into one level where it can; the nesting differs, the
157 * SQL WP_Meta_Query builds from it does not.
158 */
159 $args['meta_query'] = empty( $args['meta_query'] ) // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Mirrors the v1 lane's `last_update` filter; the direct lane is the frozen authority.
160 ? array( $last_update )
161 : array(
162 'relation' => 'AND',
163 array( $last_update ),
164 $args['meta_query'],
165 );
166 }
167 if ( isset( $this->delegated['roles'] ) ) {
168 // `role__in` and `role` are mutually exclusive in WP_User_Query; v1 drops
169 // `role` for the same reason, so an explicit `role` cannot re-narrow the set.
170 $args['role__in'] = $this->delegated['roles'];
171 unset( $args['role'] );
172 }
173
174 return $args;
175 }
176
177 /**
178 * Add per-term user-table and customer-meta search clauses.
179 *
180 * @param WP_User_Query $query User query.
181 */
182 public function search_user_table( WP_User_Query $query ): void {
183 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names come from $wpdb; $placeholders is a generated list of %s placeholders, and the keys themselves are passed to prepare() as arguments.
184 global $wpdb;
185
186 if ( empty( $query->query_vars['_wcpos_search'] ) ) {
187 return;
188 }
189 $terms = preg_split( '/\s+/u', (string) $query->query_vars['_wcpos_search'], -1, PREG_SPLIT_NO_EMPTY );
190 if ( false === $terms || empty( $terms ) ) {
191 $query->query_where .= ' AND 1 = 0';
192 return;
193 }
194
195 $terms = array_slice( $terms, 0, 10 );
196 $meta_keys = array_merge(
197 array( 'first_name', 'last_name', 'billing_first_name', 'billing_last_name', 'billing_email', 'billing_company', 'billing_phone' ),
198 Tax_Id_Reader::fallback_user_meta_keys()
199 );
200 $placeholders = implode( ', ', array_fill( 0, \count( $meta_keys ), '%s' ) );
201 $groups = array();
202 foreach ( $terms as $term ) {
203 $like = '%' . $wpdb->esc_like( $term ) . '%';
204 $groups[] = $wpdb->prepare(
205 "( {$wpdb->users}.user_email LIKE %s
206 OR {$wpdb->users}.user_login LIKE %s
207 OR {$wpdb->users}.display_name LIKE %s
208 OR EXISTS (
209 SELECT 1 FROM {$wpdb->usermeta} AS wcpos_search_meta
210 WHERE wcpos_search_meta.user_id = {$wpdb->users}.ID
211 AND wcpos_search_meta.meta_key IN ($placeholders)
212 AND wcpos_search_meta.meta_value LIKE %s
213 )
214 )",
215 array_merge( array( $like, $like, $like ), $meta_keys, array( $like ) )
216 );
217 }
218 $query->query_where .= ' AND ( ' . implode( ' AND ', $groups ) . ' )';
219 }
220
221 /**
222 * Sort customer queries by the existing WCPOS role hierarchy.
223 *
224 * @param WP_User_Query $query User query.
225 */
226 public function orderby_role( WP_User_Query $query ): void {
227 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names come from $wpdb; $when_sql is built from %s placeholders whose values are passed to prepare() as arguments.
228 global $wpdb;
229
230 if ( empty( $query->query_vars['_wcpos_orderby_role'] ) ) {
231 return;
232 }
233 $order = isset( $query->query_vars['order'] ) && 'DESC' === strtoupper( (string) $query->query_vars['order'] ) ? 'DESC' : 'ASC';
234 $hierarchy = array( 'administrator', 'shop_manager', 'cashier', 'editor', 'author', 'contributor', 'customer', 'subscriber' );
235 $cap_key = $wpdb->get_blog_prefix() . 'capabilities';
236 if ( false === strpos( $query->query_from, 'wcpos_role_meta' ) ) {
237 $query->query_from .= $wpdb->prepare(
238 " LEFT JOIN {$wpdb->usermeta} AS wcpos_role_meta ON ( {$wpdb->users}.ID = wcpos_role_meta.user_id AND wcpos_role_meta.meta_key = %s )",
239 $cap_key
240 );
241 }
242
243 $when_sql = '';
244 $when_args = array();
245 $rank = 1;
246 foreach ( $hierarchy as $role ) {
247 $when_sql .= ' WHEN wcpos_role_meta.meta_value LIKE %s THEN ' . $rank;
248 $when_args[] = '%' . $wpdb->esc_like( '"' . $role . '"' ) . '%';
249 ++$rank;
250 }
251 $order_by = $wpdb->prepare( "CASE{$when_sql} ELSE {$rank} END", $when_args );
252 $query->query_orderby = "ORDER BY ( {$order_by} ) {$order}, {$wpdb->users}.user_login ASC";
253 }
254 }
255