PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
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
← All changes | includes/API.php +173 -35 1.10.191.10.18 View file →
@@ -9,9 +9,8 @@
9 9 */
10 10
11 11 namespace WCPOS\WooCommercePOS;
12 12
13 -use WCPOS\WooCommercePOS\API\Controller_Registry;
14 13 use WCPOS\WooCommercePOS\Services\Auth;
15 14 use WCPOS\WooCommercePOS\Services\Client_Signal;
16 15 use WCPOS\WooCommercePOS\Services\Settings as SettingsService;
17 16 use WP_HTTP_Response;
@@ -28,15 +27,23 @@
28 27 */
29 28 public const ROUTE_NAMESPACES = array( 'wcpos/v1', 'wcpos/v2' );
30 29
31 30 /**
32 - * Controller instances and route attribution.
31 + * WCPOS REST API namespaces and endpoints.
33 32 *
34 - * @var Controller_Registry
33 + * @var array
35 34 */
36 - protected Controller_Registry $registry;
35 + protected $controllers = array();
37 36
38 37 /**
38 + * Map of route patterns to controller keys.
39 + * Built during register_routes() for use in rest_dispatch_request().
40 + *
41 + * @var array<string, string>
42 + */
43 + protected $route_map = array();
44 +
45 + /**
39 46 * Route permission-gate classifier.
40 47 *
41 48 * @var API\Route_Classifier
42 49 */
@@ -49,15 +56,8 @@
49 56 */
50 57 protected $is_auth_checked = false;
51 58
52 59 /**
53 - * Validation error for a token presented during this request.
54 - *
55 - * @var \WP_Error|null
56 - */
57 - private $auth_error = null;
58 -
59 - /**
60 60 * Flag to track whether WCPOS successfully authenticated the current request
61 61 * via its own Bearer token. Used to suppress errors from third-party JWT
62 62 * plugins that inspected the same Authorization header but could not validate
63 63 * a WCPOS-issued token with their own secret.
@@ -119,13 +119,167 @@
119 119 public function register_routes(): void {
120 120 $route_namespaces = $this->get_route_namespaces();
121 121 $this->route_classifier = new API\Route_Classifier( $route_namespaces );
122 122
123 - $this->registry = new Controller_Registry();
124 - $this->registry->register( $this->route_classifier );
123 + /**
124 + * Filter the list of controller classes used in the WCPOS REST API.
125 + *
126 + * This filter allows customizing or extending the set of controller classes that handle
127 + * REST API routes for the WCPOS. By filtering these controllers, plugins can
128 + * modify existing endpoints or add new controllers for additional functionality.
129 + * Core legacy controllers use their versioned WCPOS\WooCommercePOS\API\V1 FQCNs.
130 + *
131 + * @since 1.5.0
132 + *
133 + * @param array $controllers Associative array of controller identifiers to their corresponding class names.
134 + * - 'auth' => Fully qualified name of the class handling authentication.
135 + * - 'settings' => Fully qualified name of the class handling settings.
136 + * - 'cashier' => Fully qualified name of the class handling cashier management.
137 + * - 'products' => Fully qualified name of the class handling products.
138 + * - 'product_variations' => Fully qualified name of the class handling product variations.
139 + * - 'orders' => Fully qualified name of the class handling orders.
140 + * - 'customers' => Fully qualified name of the class handling customers.
141 + * - 'product_tags' => Fully qualified name of the class handling product tags.
142 + * - 'product_categories' => Fully qualified name of the class handling product categories.
143 + * - 'taxes' => Fully qualified name of the class handling taxes.
144 + * - 'shipping_methods' => Fully qualified name of the class handling shipping methods.
145 + * - 'tax_classes' => Fully qualified name of the class handling tax classes.
146 + * - 'order_statuses' => Fully qualified name of the class handling order statuses.
147 + */
148 + $classes = apply_filters(
149 + 'woocommerce_pos_rest_api_controllers',
150 + array(
151 + // WCPOS rest api controllers.
152 + 'auth' => API\V1\Auth::class,
153 + 'settings' => API\V1\Settings::class,
154 + 'cashier' => API\V1\Cashier::class,
155 + 'templates' => API\V1\Templates_Controller::class,
156 + 'receipts' => API\V1\Receipts_Controller::class,
157 + 'print_jobs' => API\V1\Print_Jobs_Controller::class,
125 158
159 + // TODO: remove this?
160 + 'stores' => API\V1\Stores::class,
161 + 'extensions' => API\V1\Extensions::class,
162 + 'logs' => API\V1\Logs::class,
163 + 'payment_gateways' => API\V1\Payment_Gateways::class,
164 + 'gateway_bootstrap' => API\V1\Gateway_Bootstrap_Controller::class,
165 + 'checkout' => API\V1\Checkout_Controller::class,
166 +
167 + // extend WC REST API controllers.
168 + 'products' => API\V1\Products_Controller::class,
169 + 'product_variations' => API\V1\Product_Variations_Controller::class,
170 + 'orders' => API\V1\Orders_Controller::class,
171 + 'customers' => API\V1\Customers_Controller::class,
172 + 'product_tags' => API\V1\Product_Tags_Controller::class,
173 + 'product_categories' => API\V1\Product_Categories_Controller::class,
174 + 'product_brands' => API\V1\Product_Brands_Controller::class,
175 + 'coupons' => API\V1\Coupons_Controller::class,
176 + 'taxes' => API\V1\Taxes_Controller::class,
177 + 'shipping_methods' => API\V1\Shipping_Methods_Controller::class,
178 + 'tax_classes' => API\V1\Tax_Classes_Controller::class,
179 + 'order_statuses' => API\V1\Data_Order_Statuses_Controller::class,
180 + )
181 + );
182 +
183 + /**
184 + * Filter the wcpos/v2 service pass-through controllers (additive to the
185 + * frozen v1 surface — the legacy data controllers stay v1-only).
186 + *
187 + * Extensions that replace a v1 service through
188 + * `woocommerce_pos_rest_api_controllers` must carry their service onto
189 + * the v2 surface here with their own pass-through subclass (override
190 + * `$namespace = 'wcpos/v2'`), exactly as core does — the v2 map is not
191 + * derived from the v1 map, so a v1 replacement alone leaves the v2
192 + * twin serving core behavior.
193 + *
194 + * @since 1.10.0
195 + *
196 + * @param array $controllers Associative array of v2 service controller class names.
197 + */
198 + $v2_classes = apply_filters(
199 + 'woocommerce_pos_rest_api_v2_controllers',
200 + array(
201 + 'ping' => API\V2\Ping::class,
202 + 'echo_probe' => API\V2\Echo_Probe::class,
203 + 'site' => API\V2\Site::class,
204 + 'auth' => API\V2\Auth::class,
205 + 'settings' => API\V2\Settings::class,
206 + 'cashier' => API\V2\Cashier::class,
207 + 'templates' => API\V2\Templates_Controller::class,
208 + 'receipts' => API\V2\Receipts_Controller::class,
209 + 'print_jobs' => API\V2\Print_Jobs_Controller::class,
210 + 'stores' => API\V2\Stores::class,
211 + 'extensions' => API\V2\Extensions::class,
212 + 'logs' => API\V2\Logs::class,
213 + 'payment_gateways' => API\V2\Payment_Gateways::class,
214 + 'gateway_bootstrap' => API\V2\Gateway_Bootstrap_Controller::class,
215 + 'checkout' => API\V2\Checkout_Controller::class,
216 + 'order_email' => API\V2\Order_Email_Controller::class,
217 + 'shipping_methods' => API\V2\Shipping_Methods_Controller::class,
218 + 'tax_classes' => API\V2\Tax_Classes_Controller::class,
219 + 'order_statuses' => API\V2\Data_Order_Statuses_Controller::class,
220 + )
221 + );
222 + foreach ( $v2_classes as $key => $class ) {
223 + $classes[ 'v2-' . $key ] = $class;
224 + }
225 + $legacy_classifications = array(
226 + 'auth' => array( 'public' => array( '/wcpos/v1/auth/test', '/wcpos/v1/auth/refresh' ) ),
227 + 'print_jobs' => array( 'printer_token' => array( '/wcpos/v1/print-jobs/cloudprnt', '/wcpos/v1/print-jobs/epson-sdp' ) ),
228 + 'receipts' => array( 'permission_error_passthrough' => array( '/wcpos/v1/receipts/' ) ),
229 + );
230 +
231 + foreach ( $classes as $key => $class ) {
232 + if ( class_exists( $class ) ) {
233 + $this->controllers[ $key ] = new $class();
234 + $this->controllers[ $key ]->register_routes();
235 +
236 + if ( method_exists( $this->controllers[ $key ], 'wcpos_route_classifications' ) ) {
237 + $this->route_classifier->merge( $this->controllers[ $key ]->wcpos_route_classifications() );
238 + } elseif ( isset( $legacy_classifications[ $key ] ) ) {
239 + $this->route_classifier->merge( $legacy_classifications[ $key ] );
240 + }
241 + }
242 + }
243 +
126 244 // Sync classifications are independent of feature-gated route registration.
127 245 $this->route_classifier->merge( Sync\Api::route_classifications() );
246 +
247 + // Build route map for use in rest_dispatch_request().
248 + $rest_server = rest_get_server();
249 +
250 + foreach ( $route_namespaces as $route_namespace ) {
251 + $all_routes = $rest_server->get_routes( $route_namespace );
252 +
253 + foreach ( $all_routes as $route_pattern => $route_handlers ) {
254 + foreach ( $route_handlers as $route_handler ) {
255 + $callback = $route_handler['callback'] ?? null;
256 +
257 + // Extract the controller object from the callback.
258 + $controller_obj = null;
259 + if ( \is_array( $callback ) && isset( $callback[0] ) && \is_object( $callback[0] ) ) {
260 + $controller_obj = $callback[0];
261 + } elseif ( $callback instanceof \Closure ) {
262 + // WC 10.5+ RestApiCache wraps callbacks in closures.
263 + // Use reflection to extract the bound $this.
264 + $ref = new \ReflectionFunction( $callback );
265 + $controller_obj = $ref->getClosureThis();
266 + }
267 +
268 + if ( ! $controller_obj ) {
269 + continue;
270 + }
271 +
272 + // Find which controller key this object belongs to.
273 + foreach ( $this->controllers as $key => $registered_controller ) {
274 + if ( $controller_obj === $registered_controller ) {
275 + $this->route_map[ $route_pattern ] = $key;
276 + break;
277 + }
278 + }
279 + }
280 + }
281 + }
128 282 }
129 283
130 284 /**
131 285 * Check request for any login tokens.
@@ -391,27 +545,12 @@
391 545
392 546 if ( ! $is_public_auth_route && ! $has_route_specific_permission_error && ! $is_printer_token_route && ! $is_sync_admin_route ) {
393 547 if ( ! current_user_can( 'access_woocommerce_pos' ) ) {
394 548 if ( ! is_user_logged_in() ) {
395 - $data = array( 'status' => 401 );
396 - if ( null !== $this->auth_error ) {
397 - $data['reason'] = $this->auth_error->get_error_code();
398 - if ( 'woocommerce_pos_auth_token_expired' !== $data['reason'] ) {
399 - Logger::warning(
400 - 'POS request refused: ' . $data['reason'] . ' — ' . $this->auth_error->get_error_message(),
401 - array(
402 - 'route' => $route,
403 - 'method' => $request->get_method(),
404 - 'reason' => $data['reason'],
405 - )
406 - );
407 - }
408 - }
409 -
410 549 return new \WP_Error(
411 550 'woocommerce_pos_rest_unauthorized',
412 551 __( 'Authentication required.', 'woocommerce-pos' ),
413 - $data
552 + array( 'status' => 401 )
414 553 );
415 554 }
416 555
417 556 return new \WP_Error(
@@ -491,10 +630,9 @@
491 630 * @return mixed
492 631 */
493 632 public function rest_dispatch_request( $dispatch_result, $request, $route, $handler ) {
494 633 // Only process mapped WCPOS routes.
495 - $controller = $this->registry->controller_for_route( $route );
496 - if ( null === $controller ) {
634 + if ( ! isset( $this->route_map[ $route ] ) ) {
497 635 return $dispatch_result;
498 636 }
499 637
500 638 /*
@@ -513,9 +651,12 @@
513 651 @ini_set( 'display_errors', '0' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed -- intentionally disabling error display for POS API responses.
514 652 @ini_set( 'precision', '10' );
515 653 @ini_set( 'serialize_precision', '10' );
516 654
517 - if ( method_exists( $controller, 'wcpos_dispatch_request' ) ) {
655 + $key = $this->route_map[ $route ];
656 + $controller = $this->controllers[ $key ] ?? null;
657 +
658 + if ( $controller && method_exists( $controller, 'wcpos_dispatch_request' ) ) {
518 659 return $controller->wcpos_dispatch_request( $dispatch_result, $request, $route, $handler );
519 660 }
520 661
521 662 return $dispatch_result;
@@ -567,14 +708,11 @@
567 708 *
568 709 * @return false|int|\WP_Error
569 710 */
570 711 private function authenticate( $user_id ) {
571 - // Per-request: never let a previous authentication's verdict describe this one.
572 - $this->auth_error = null;
573 712 $authenticated_user_id = Auth::instance()->authenticate_request();
574 713
575 714 if ( is_wp_error( $authenticated_user_id ) ) {
576 - $this->auth_error = $authenticated_user_id;
577 715 return false === $user_id ? $authenticated_user_id : $user_id;
578 716 }
579 717
580 718 return false === $authenticated_user_id ? $user_id : $authenticated_user_id;