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 +92 -21 1.10.21.10.18 View file →
@@ -10,8 +10,9 @@
10 10
11 11 namespace WCPOS\WooCommercePOS;
12 12
13 13 use WCPOS\WooCommercePOS\Services\Auth;
14 +use WCPOS\WooCommercePOS\Services\Client_Signal;
14 15 use WCPOS\WooCommercePOS\Services\Settings as SettingsService;
15 16 use WP_HTTP_Response;
16 17 use WP_REST_Request;
17 18 use WP_REST_Response;
@@ -83,8 +84,9 @@
83 84
84 85 // These filters allow changes to the WC REST API response.
85 86 add_filter( 'rest_dispatch_request', array( $this, 'rest_dispatch_request' ), 10, 4 );
86 87 add_filter( 'rest_pre_dispatch', array( $this, 'rest_pre_dispatch' ), 10, 3 );
88 + add_filter( 'rest_pre_dispatch', array( $this, 'clear_third_party_jwt_error' ), 50, 3 );
87 89 add_filter( 'rest_post_dispatch', array( $this, 'rest_post_dispatch' ), 10, 3 );
88 90 }
89 91
90 92 /**
@@ -328,13 +330,14 @@
328 330 * 1. WooCommerce issue #26847: determine_current_user may not be called when
329 331 * WordPress has already cached the current user. We attempt auth here as a
330 332 * fallback.
331 333 *
332 - * 2. JWT plugin conflict: a third-party JWT plugin (e.g. jwt-authentication-for-wp-rest-api)
333 - * sees our Bearer token, fails to validate it with its own secret, and returns
334 - * a WP_Error via rest_authentication_errors at priority 10. We run at priority 50
335 - * and attempt our own Bearer-token validation. If it succeeds, we clear the
336 - * stale error — our authentication wins.
334 + * 2. JWT plugin conflict: a third-party JWT plugin sees our Bearer token, fails
335 + * to validate it with its own secret, and returns a WP_Error via
336 + * rest_authentication_errors at priority 10. We run at priority 50 and attempt
337 + * our own Bearer-token validation. If it succeeds, we clear the stale error —
338 + * our authentication wins. (jwt-authentication-for-wp-rest-api surfaces its
339 + * error through rest_pre_dispatch instead; see clear_third_party_jwt_error().)
337 340 *
338 341 * @param mixed $errors Authentication errors.
339 342 *
340 343 * @return mixed
@@ -347,19 +350,9 @@
347 350 if ( ! empty( $errors ) ) {
348 351 // Only clear errors that originate from JWT authentication plugins. Errors
349 352 // from other mechanisms (maintenance locks, IP restrictions, etc.) should
350 353 // be passed through even when the WCPOS Bearer token is valid.
351 - $is_jwt_plugin_error = is_wp_error( $errors ) && 0 === strpos( $errors->get_error_code(), 'jwt_auth_' );
352 -
353 - if ( $is_jwt_plugin_error && ! $this->authenticated_via_wcpos ) {
354 - $user_id = $this->authenticate( false );
355 - if ( $user_id && ! is_wp_error( $user_id ) ) {
356 - wp_set_current_user( $user_id );
357 - $this->authenticated_via_wcpos = true;
358 - }
359 - }
360 -
361 - if ( $this->authenticated_via_wcpos && $is_jwt_plugin_error ) {
354 + if ( $this->is_third_party_jwt_error( $errors ) && $this->ensure_authenticated_via_wcpos() ) {
362 355 return null;
363 356 }
364 357
365 358 return $errors;
@@ -365,20 +358,80 @@
365 358 return $errors;
366 359 }
367 360
368 361 // check if determine_current_user has been called.
369 - if ( ! $this->is_auth_checked ) {
370 - // Authentication hasn't occurred during `determine_current_user`, so check auth.
362 + if ( ! $this->is_auth_checked && $this->ensure_authenticated_via_wcpos() ) {
363 + // Authentication hadn't occurred during `determine_current_user`, but our token is valid.
364 + return true;
365 + }
366 +
367 + return $errors;
368 + }
369 +
370 + /**
371 + * Clear a third-party JWT plugin's stale error from the dispatch result.
372 + *
373 + * The plugin jwt-authentication-for-wp-rest-api (verified at 1.5.0) validates every
374 + * Bearer token in determine_current_user (priority 10) with its own secret. Ours fails,
375 + * so it stores a `jwt_auth_invalid_token` WP_Error and returns the user untouched;
376 + * our priority-20 filter then authenticates the request. The plugin later returns
377 + * that stored error from rest_pre_dispatch (priority 10, registered at
378 + * plugins_loaded), which replaces the dispatch result with a 403.
379 + *
380 + * Priority 50: after the plugin's callback, and after our own priority-10
381 + * permission gate, whose `woocommerce_pos_rest_*` errors must pass through untouched.
382 + *
383 + * Unlike rest_authentication_errors(), this never switches the current user: the
384 + * priority-10 gate and the core-order audit guard have already judged the user in
385 + * scope, so the error is cleared only when our token resolves to that same user.
386 + *
387 + * @param mixed $result Dispatch result, or null to not hijack the request.
388 + * @param WP_REST_Server $server Server instance.
389 + * @param WP_REST_Request $request Request used to generate the response.
390 + *
391 + * @return mixed
392 + */
393 + public function clear_third_party_jwt_error( $result, $server, $request ) {
394 + if ( ! $this->is_third_party_jwt_error( $result ) ) {
395 + return $result;
396 + }
397 +
398 + if ( ! $this->authenticated_via_wcpos ) {
371 399 $user_id = $this->authenticate( false );
400 + if ( $user_id && ! is_wp_error( $user_id ) && get_current_user_id() === (int) $user_id ) {
401 + $this->authenticated_via_wcpos = true;
402 + }
403 + }
404 +
405 + return $this->authenticated_via_wcpos ? null : $result;
406 + }
407 +
408 + /**
409 + * Whether a value is a WP_Error raised by a third-party JWT plugin (`jwt_auth_*`).
410 + *
411 + * @param mixed $maybe_error Value to inspect.
412 + *
413 + * @return bool
414 + */
415 + private function is_third_party_jwt_error( $maybe_error ): bool {
416 + return is_wp_error( $maybe_error ) && 0 === strpos( $maybe_error->get_error_code(), 'jwt_auth_' );
417 + }
418 +
419 + /**
420 + * Authenticate the request with its WCPOS Bearer token if that hasn't happened yet.
421 + *
422 + * @return bool True when the request is authenticated via a WCPOS-issued token.
423 + */
424 + private function ensure_authenticated_via_wcpos(): bool {
425 + if ( ! $this->authenticated_via_wcpos ) {
426 + $user_id = $this->authenticate( false );
372 427 if ( $user_id && ! is_wp_error( $user_id ) ) {
373 428 wp_set_current_user( $user_id );
374 429 $this->authenticated_via_wcpos = true;
375 -
376 - return true;
377 430 }
378 431 }
379 432
380 - return $errors;
433 + return $this->authenticated_via_wcpos;
381 434 }
382 435
383 436 /**
384 437 * Extract the Authorization Bearer token from the request.
@@ -442,8 +495,26 @@
442 495 */
443 496 public function rest_pre_dispatch( $result, $server, $request ) {
444 497 if ( ! $this->route_classifier->in_wcpos_namespace( $request->get_route() ) ) {
445 498 return $result;
499 + }
500 +
501 + // Marker-gated on purpose (query var or header, NOT the rest_route arm,
502 + // which matches this namespace by construction): every real POS client,
503 + // old or new, carries the marker, while unmarked scanner traffic would
504 + // otherwise inflate the `channel: none` tail this telemetry exists to
505 + // measure (free#1752). The echo and auth lanes are excluded for the same
506 + // reason: they are the gate's carve-outs, and a protocol-2 client's
507 + // connect-time probes deliberately carry no signal — counting them would
508 + // stamp every modern client with a daily false `none` row.
509 + if ( 0 === stripos( $request->get_route(), '/wcpos/v2/' )
510 + && 1 !== preg_match( '#^/wcpos/v2/(?:echo$|auth(?:/|$))#i', $request->get_route() )
511 + && ( wcpos_request( 'query_var' ) || wcpos_request( 'header' ) ) ) {
512 + try {
513 + Client_Signal::record( $request );
514 + } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Telemetry failures are deliberately ignored.
515 + // Telemetry must never interrupt a POS request.
516 + }
446 517 }
447 518
448 519 // Latch the till's store scope for the whole request (pro#425). Set
449 520 // unconditionally — including to null — so a scope never leaks from one