PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.16
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.16
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 / V1 / Traits / Term_Controller.php

Term_Controller.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.16, at includes/API/V1/Traits/Term_Controller.php

255 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Term_Controller.
4 *
5 * @package WCPOS\WooCommercePOS
6 */
7
8 namespace WCPOS\WooCommercePOS\API\V1\Traits;
9
10 use Exception;
11 use WCPOS\WooCommercePOS\API\V1\Bulk_ID_Fast_Path;
12 use WCPOS\WooCommercePOS\API\V2\Proxy\Stable_Sort;
13 use WP_Error;
14 use WP_REST_Request;
15 use WP_REST_Response;
16
17 /**
18 * Shared behaviour for the wcpos/v1 term collections.
19 *
20 * Tags, categories and brands each have to extend a DIFFERENT
21 * WC_REST_Product_*_Controller to inherit its schema, routes and permission
22 * checks, so the shared behaviour cannot live in a base class. Each using
23 * class supplies four constants:
24 *
25 * - WCPOS_TAXONOMY the taxonomy name, which also builds both WC hook names;
26 * - WCPOS_RESPONSE_FILTER the method registered on woocommerce_rest_prepare_{taxonomy};
27 * - WCPOS_QUERY_FILTER the method registered on woocommerce_rest_{taxonomy}_query;
28 * - WCPOS_ID_ERROR_LABEL the collection label used in fast-path error messages.
29 *
30 * The two filter methods stay in the subclasses on purpose: third-party code
31 * detaches them by their historical `array( $controller, 'wcpos_product_tags_response' )`
32 * tuple, so those exact method names are the registered callbacks.
33 */
34 trait Term_Controller {
35 /**
36 * Store the request object for use in lifecycle methods.
37 *
38 * @var WP_REST_Request|null
39 */
40 protected $wcpos_request;
41
42 /**
43 * Requests currently using this controller's temporary filters.
44 *
45 * @var WP_REST_Request[]
46 */
47 protected $wcpos_request_stack = array();
48
49 /**
50 * Dispatch request to parent controller, or override if needed.
51 *
52 * @param mixed $dispatch_result Dispatch result, will be used if not empty.
53 * @param WP_REST_Request $request Request used to generate the response.
54 * @param string $route Route matched for the request.
55 * @param array $handler Route handler used for the request.
56 *
57 * @return mixed
58 */
59 public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $request, $route, $handler ) {
60 $this->wcpos_request_stack[] = $request;
61 $this->wcpos_request = $request;
62 $taxonomy = static::WCPOS_TAXONOMY;
63
64 add_filter( "woocommerce_rest_prepare_{$taxonomy}", array( $this, static::WCPOS_RESPONSE_FILTER ), 10, 3 );
65 add_filter( "woocommerce_rest_{$taxonomy}_query", array( $this, static::WCPOS_QUERY_FILTER ), 10, 2 );
66 add_filter( 'rest_request_after_callbacks', array( $this, 'wcpos_remove_term_filters' ), 10, 3 );
67
68 /*
69 * Check if the request is for all terms and if the 'posts_per_page' is set to -1.
70 * Optimised query for getting all term IDs.
71 */
72 if ( Bulk_ID_Fast_Path::supports_request( $request ) ) {
73 return $this->wcpos_get_all_posts( $request );
74 }
75
76 return $dispatch_result;
77 }
78
79 /**
80 * Detach this request's term hooks once its callbacks have run.
81 *
82 * Nothing removed these before, so a POS term read re-wrote every later
83 * get_terms() in the same PHP request — a plain wc/v3 term read included —
84 * with this request's WHERE clause and ORDER BY tiebreak.
85 *
86 * The clauses filter cannot remove itself from inside its own callback the
87 * way Customers_Controller::wcpos_include_exclude_users_by_id() does:
88 * WC_REST_Terms_Controller::get_items() runs get_terms() and THEN a second
89 * wp_count_terms() query for X-WP-Total, and both have to carry the same
90 * wcpos_include/wcpos_exclude WHERE clause or the client is handed a total
91 * for the whole taxonomy and walks pages that hold nothing.
92 * rest_request_after_callbacks is the first point where both are done.
93 *
94 * @param mixed $response Result to send to the client.
95 * @param array $handler Route handler used for the request.
96 * @param WP_REST_Request $request Request used to generate the response.
97 *
98 * @return mixed
99 */
100 public function wcpos_remove_term_filters( $response, $handler, $request ) {
101 // A nested REST request would otherwise tear down the outer one's hooks.
102 if ( $request !== $this->wcpos_request ) {
103 return $response;
104 }
105
106 array_pop( $this->wcpos_request_stack );
107 if ( ! empty( $this->wcpos_request_stack ) ) {
108 $this->wcpos_request = end( $this->wcpos_request_stack );
109
110 return $response;
111 }
112
113 $this->wcpos_request = null;
114
115 $taxonomy = static::WCPOS_TAXONOMY;
116
117 remove_filter( 'terms_clauses', array( $this, 'wcpos_terms_clauses_include_exclude' ), 10 );
118 remove_filter( "woocommerce_rest_prepare_{$taxonomy}", array( $this, static::WCPOS_RESPONSE_FILTER ), 10 );
119 remove_filter( "woocommerce_rest_{$taxonomy}_query", array( $this, static::WCPOS_QUERY_FILTER ), 10 );
120 remove_filter( 'rest_request_after_callbacks', array( $this, 'wcpos_remove_term_filters' ), 10 );
121
122 return $response;
123 }
124
125 /**
126 * Filters the terms query SQL clauses.
127 *
128 * @param string[] $clauses {
129 * Associative array of the clauses for the query.
130 *
131 * @type string $fields The SELECT clause of the query.
132 * @type string $join The JOIN clause of the query.
133 * @type string $where The WHERE clause of the query.
134 * @type string $distinct The DISTINCT clause of the query.
135 * @type string $orderby The ORDER BY clause of the query.
136 * @type string $order The ORDER clause of the query.
137 * @type string $limits The LIMIT clause of the query.
138 * }
139 * @param string[] $taxonomies An array of taxonomy names.
140 * @param array $args An array of term query arguments.
141 *
142 * @return string[] $clauses
143 */
144 public function wcpos_terms_clauses_include_exclude( array $clauses, array $taxonomies, array $args ) {
145 global $wpdb;
146
147 // WC prepares links and third parties hook the response; only this
148 // controller's own taxonomy may be narrowed or re-sorted here.
149 if ( ! \in_array( static::WCPOS_TAXONOMY, $taxonomies, true ) ) {
150 return $clauses;
151 }
152
153 // Handle 'wcpos_include'.
154 if ( ! empty( $this->wcpos_request['wcpos_include'] ) ) {
155 $include_ids = array_map( 'intval', (array) $this->wcpos_request['wcpos_include'] );
156 $ids_format = implode( ',', array_fill( 0, \count( $include_ids ), '%d' ) );
157 $clauses['where'] .= $wpdb->prepare( " AND t.term_id IN ($ids_format) ", $include_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $ids_format is a safe placeholder string.
158 }
159
160 // Handle 'wcpos_exclude'.
161 if ( ! empty( $this->wcpos_request['wcpos_exclude'] ) ) {
162 $exclude_ids = array_map( 'intval', (array) $this->wcpos_request['wcpos_exclude'] );
163 $ids_format = implode( ',', array_fill( 0, \count( $exclude_ids ), '%d' ) );
164 $clauses['where'] .= $wpdb->prepare( " AND t.term_id NOT IN ($ids_format) ", $exclude_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $ids_format is a safe placeholder string.
165 }
166
167 /*
168 * Read Lane parity: the POS client's term lanes default to name
169 * ascending and walk multi-page windows, so a tie at a page boundary
170 * needs a deterministic secondary key. The wcpos/v2 proxy lane pins
171 * this in Terms_Proxy_Behavior; the direct lane has to carry the same
172 * Collection Rule or the two lanes paginate differently (mono#1372).
173 * No-ops on the count query, whose ORDER BY is empty.
174 */
175 return Stable_Sort::with_term_id_tiebreak( $clauses );
176 }
177
178 /**
179 * Returns array of all term ids for this controller's taxonomy.
180 *
181 * @param WP_REST_Request $request Full details about the request.
182 *
183 * @return WP_Error|WP_REST_Response
184 */
185 public function wcpos_get_all_posts( $request ) {
186 $start_time = microtime( true );
187 $modified_after = $request->get_param( 'modified_after' );
188 $label = static::WCPOS_ID_ERROR_LABEL;
189
190 $args = array(
191 'taxonomy' => static::WCPOS_TAXONOMY,
192 'hide_empty' => false,
193 'fields' => 'ids',
194 );
195 $args = Bulk_ID_Fast_Path::apply_id_filters_to_args( $args, $request );
196
197 try {
198 /**
199 * Get all term IDs for the taxonomy.
200 *
201 * @TODO - terms don't have a modified date, it would be good to add a term_meta for last_update
202 * - ideally WooCommerce would provide a modified_after filter for terms
203 * - for now we'll just return empty for modified terms
204 */
205 $ids = $modified_after ? array() : get_terms( $args );
206 if ( $ids instanceof WP_Error ) {
207 return Bulk_ID_Fast_Path::fetch_error( "Error fetching {$label} IDs: " . $ids->get_error_message(), "Error fetching {$label} IDs." );
208 }
209
210 $results = Bulk_ID_Fast_Path::rows_from_ids( $ids );
211
212 return Bulk_ID_Fast_Path::response( $this, $results, $start_time );
213 } catch ( Exception $e ) {
214 return Bulk_ID_Fast_Path::fetch_error( "Error fetching {$label} IDs: " . $e->getMessage(), "Error fetching {$label} IDs." );
215 }
216 }
217
218 /**
219 * Add the POS uuid to a prepared term response.
220 *
221 * @param WP_REST_Response $response The response object.
222 * @param object $item The original term object.
223 *
224 * @return WP_REST_Response The response object.
225 */
226 protected function wcpos_term_response( WP_REST_Response $response, object $item ): WP_REST_Response {
227 $data = $response->get_data();
228
229 // Make sure the term has a uuid.
230 $data['uuid'] = $this->get_term_uuid( $item );
231
232 // Reset the new response data.
233 $response->set_data( $data );
234
235 return $response;
236 }
237
238 /**
239 * Install the SQL clauses filter for a term collection read.
240 *
241 * The filter goes on for EVERY list read, not only the ones carrying
242 * wcpos_include/wcpos_exclude: the stable name tiebreak has to apply to
243 * plain paginated reads too, which is what the v2 proxy lane already does.
244 *
245 * @param array $args Query arguments.
246 *
247 * @return array
248 */
249 protected function wcpos_term_query( array $args ): array {
250 add_filter( 'terms_clauses', array( $this, 'wcpos_terms_clauses_include_exclude' ), 10, 3 );
251
252 return $args;
253 }
254 }
255