PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.19
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.19
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 +35 -173 1.10.131.10.19 View file →
@@ -9,8 +9,9 @@
9 9 */
10 10
11 11 namespace WCPOS\WooCommercePOS;
12 12
13 +use WCPOS\WooCommercePOS\API\Controller_Registry;
13 14 use WCPOS\WooCommercePOS\Services\Auth;
14 15 use WCPOS\WooCommercePOS\Services\Client_Signal;
15 16 use WCPOS\WooCommercePOS\Services\Settings as SettingsService;
16 17 use WP_HTTP_Response;
@@ -27,23 +28,15 @@
27 28 */
28 29 public const ROUTE_NAMESPACES = array( 'wcpos/v1', 'wcpos/v2' );
29 30
30 31 /**
31 - * WCPOS REST API namespaces and endpoints.
32 + * Controller instances and route attribution.
32 33 *
33 - * @var array
34 + * @var Controller_Registry
34 35 */
35 - protected $controllers = array();
36 + protected Controller_Registry $registry;
36 37
37 38 /**
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 - /**
46 39 * Route permission-gate classifier.
47 40 *
48 41 * @var API\Route_Classifier
49 42 */
@@ -56,8 +49,15 @@
56 49 */
57 50 protected $is_auth_checked = false;
58 51
59 52 /**
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,167 +119,13 @@
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 - /**
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,
123 + $this->registry = new Controller_Registry();
124 + $this->registry->register( $this->route_classifier );
158 125
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 -
244 126 // Sync classifications are independent of feature-gated route registration.
245 127 $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 - }
282 128 }
283 129
284 130 /**
285 131 * Check request for any login tokens.
@@ -545,12 +391,27 @@
545 391
546 392 if ( ! $is_public_auth_route && ! $has_route_specific_permission_error && ! $is_printer_token_route && ! $is_sync_admin_route ) {
547 393 if ( ! current_user_can( 'access_woocommerce_pos' ) ) {
548 394 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 +
549 410 return new \WP_Error(
550 411 'woocommerce_pos_rest_unauthorized',
551 412 __( 'Authentication required.', 'woocommerce-pos' ),
552 - array( 'status' => 401 )
413 + $data
553 414 );
554 415 }
555 416
556 417 return new \WP_Error(
@@ -630,9 +491,10 @@
630 491 * @return mixed
631 492 */
632 493 public function rest_dispatch_request( $dispatch_result, $request, $route, $handler ) {
633 494 // Only process mapped WCPOS routes.
634 - if ( ! isset( $this->route_map[ $route ] ) ) {
495 + $controller = $this->registry->controller_for_route( $route );
496 + if ( null === $controller ) {
635 497 return $dispatch_result;
636 498 }
637 499
638 500 /*
@@ -651,12 +513,9 @@
651 513 @ini_set( 'display_errors', '0' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed -- intentionally disabling error display for POS API responses.
652 514 @ini_set( 'precision', '10' );
653 515 @ini_set( 'serialize_precision', '10' );
654 516
655 - $key = $this->route_map[ $route ];
656 - $controller = $this->controllers[ $key ] ?? null;
657 -
658 - if ( $controller && method_exists( $controller, 'wcpos_dispatch_request' ) ) {
517 + if ( method_exists( $controller, 'wcpos_dispatch_request' ) ) {
659 518 return $controller->wcpos_dispatch_request( $dispatch_result, $request, $route, $handler );
660 519 }
661 520
662 521 return $dispatch_result;
@@ -708,11 +567,14 @@
708 567 *
709 568 * @return false|int|\WP_Error
710 569 */
711 570 private function authenticate( $user_id ) {
571 + // Per-request: never let a previous authentication's verdict describe this one.
572 + $this->auth_error = null;
712 573 $authenticated_user_id = Auth::instance()->authenticate_request();
713 574
714 575 if ( is_wp_error( $authenticated_user_id ) ) {
576 + $this->auth_error = $authenticated_user_id;
715 577 return false === $user_id ? $authenticated_user_id : $user_id;
716 578 }
717 579
718 580 return false === $authenticated_user_id ? $user_id : $authenticated_user_id;