# woocommerce-pos/trunk/includes/API.php

WCPOS – Point of Sale (POS) plugin for WooCommerce, version trunk. 721 lines.

- Page: https://pluginprobe.com/plugins/woocommerce-pos/trunk/code/includes/API.php
- Raw: https://pluginprobe.com/plugins/woocommerce-pos/trunk/raw/includes/API.php
- Modified: 2026-09-06T10:00:12+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/woocommerce-pos/trunk/code/includes/API.php#L10-L20`.

```php
<?php
/**
 * WCPOS REST API class.
 *
 * @author   Paul Kilmurray <paul@kilbot.com>
 *
 * @see     http://wcpos.com
 * @package WCPOS\WooCommercePOS
 */

namespace WCPOS\WooCommercePOS;

use WCPOS\WooCommercePOS\Services\Auth;
use WCPOS\WooCommercePOS\Services\Client_Signal;
use WCPOS\WooCommercePOS\Services\Settings as SettingsService;
use WP_HTTP_Response;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * API class.
 */
class API {
	/**
	 * WCPOS REST API namespaces.
	 */
	public const ROUTE_NAMESPACES = array( 'wcpos/v1', 'wcpos/v2' );

	/**
	 * WCPOS REST API namespaces and endpoints.
	 *
	 * @var array
	 */
	protected $controllers = array();

	/**
	 * Map of route patterns to controller keys.
	 * Built during register_routes() for use in rest_dispatch_request().
	 *
	 * @var array<string, string>
	 */
	protected $route_map = array();

	/**
	 * Route permission-gate classifier.
	 *
	 * @var API\Route_Classifier
	 */
	protected $route_classifier;

	/**
	 * Flag to check if authentication has been checked.
	 *
	 * @var bool
	 */
	protected $is_auth_checked = false;

	/**
	 * Flag to track whether WCPOS successfully authenticated the current request
	 * via its own Bearer token. Used to suppress errors from third-party JWT
	 * plugins that inspected the same Authorization header but could not validate
	 * a WCPOS-issued token with their own secret.
	 *
	 * @var bool
	 */
	protected $authenticated_via_wcpos = false;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->register_routes();

		/*
		 * Adds authentication to for JWT bearer tokens
		 * - We run determine_current_user at 20 to allow other plugins to run first
		 */
		add_filter( 'determine_current_user', array( $this, 'determine_current_user' ), 20 );
		add_filter( 'rest_authentication_errors', array( $this, 'rest_authentication_errors' ), 50, 1 );

		// Adds info about the WordPress install.
		add_filter( 'rest_index', array( $this, 'rest_index' ), 10, 1 );

		// These filters allow changes to the WC REST API response.
		add_filter( 'rest_dispatch_request', array( $this, 'rest_dispatch_request' ), 10, 4 );
		add_filter( 'rest_pre_dispatch', array( $this, 'rest_pre_dispatch' ), 10, 3 );
		add_filter( 'rest_pre_dispatch', array( $this, 'clear_third_party_jwt_error' ), 50, 3 );
		add_filter( 'rest_post_dispatch', array( $this, 'rest_post_dispatch' ), 10, 3 );
	}

	/**
	 * Get the WCPOS REST API namespaces.
	 *
	 * @return string[] REST API namespaces.
	 */
	public function get_route_namespaces(): array {
		/**
		 * Filter the list of namespaces used in the WCPOS REST API.
		 *
		 * This filter is strictly additive: plugins can register additional WCPOS REST
		 * API namespaces, but the core namespaces cannot be removed — the central
		 * permission gate must keep covering every registered core route. Controllers
		 * remain responsible for declaring any special route classifications within
		 * added namespaces.
		 *
		 * @since 1.10.0
		 *
		 * @param string[] $namespaces REST API namespaces.
		 */
		$namespaces = apply_filters( 'woocommerce_pos_rest_namespaces', self::ROUTE_NAMESPACES );

		return array_values( array_unique( array_merge( self::ROUTE_NAMESPACES, (array) $namespaces ) ) );
	}

	/**
	 * Register routes for all controllers.
	 */
	public function register_routes(): void {
		$route_namespaces       = $this->get_route_namespaces();
		$this->route_classifier = new API\Route_Classifier( $route_namespaces );

		/**
		 * Filter the list of controller classes used in the WCPOS REST API.
		 *
		 * This filter allows customizing or extending the set of controller classes that handle
		 * REST API routes for the WCPOS. By filtering these controllers, plugins can
		 * modify existing endpoints or add new controllers for additional functionality.
		 * Core legacy controllers use their versioned WCPOS\WooCommercePOS\API\V1 FQCNs.
		 *
		 * @since 1.5.0
		 *
		 * @param array $controllers Associative array of controller identifiers to their corresponding class names.
		 *                           - 'auth'                  => Fully qualified name of the class handling authentication.
		 *                           - 'settings'              => Fully qualified name of the class handling settings.
		 *                           - 'cashier'               => Fully qualified name of the class handling cashier management.
		 *                           - 'products'              => Fully qualified name of the class handling products.
		 *                           - 'product_variations'    => Fully qualified name of the class handling product variations.
		 *                           - 'orders'                => Fully qualified name of the class handling orders.
		 *                           - 'customers'             => Fully qualified name of the class handling customers.
		 *                           - 'product_tags'          => Fully qualified name of the class handling product tags.
		 *                           - 'product_categories'    => Fully qualified name of the class handling product categories.
		 *                           - 'taxes'                 => Fully qualified name of the class handling taxes.
		 *                           - 'shipping_methods'      => Fully qualified name of the class handling shipping methods.
		 *                           - 'tax_classes'           => Fully qualified name of the class handling tax classes.
		 *                           - 'order_statuses'        => Fully qualified name of the class handling order statuses.
		 */
		$classes = apply_filters(
			'woocommerce_pos_rest_api_controllers',
			array(
				// WCPOS rest api controllers.
				'auth'                  => API\V1\Auth::class,
				'settings'              => API\V1\Settings::class,
				'cashier'               => API\V1\Cashier::class,
				'templates'             => API\V1\Templates_Controller::class,
				'receipts'              => API\V1\Receipts_Controller::class,
				'print_jobs'            => API\V1\Print_Jobs_Controller::class,

				// TODO: remove this?
				'stores'                => API\V1\Stores::class,
				'extensions'            => API\V1\Extensions::class,
				'logs'                  => API\V1\Logs::class,
				'payment_gateways'      => API\V1\Payment_Gateways::class,
				'gateway_bootstrap'     => API\V1\Gateway_Bootstrap_Controller::class,
				'checkout'              => API\V1\Checkout_Controller::class,

				// extend WC REST API controllers.
				'products'              => API\V1\Products_Controller::class,
				'product_variations'    => API\V1\Product_Variations_Controller::class,
				'orders'                => API\V1\Orders_Controller::class,
				'customers'             => API\V1\Customers_Controller::class,
				'product_tags'          => API\V1\Product_Tags_Controller::class,
				'product_categories'    => API\V1\Product_Categories_Controller::class,
				'product_brands'        => API\V1\Product_Brands_Controller::class,
				'coupons'               => API\V1\Coupons_Controller::class,
				'taxes'                 => API\V1\Taxes_Controller::class,
				'shipping_methods'      => API\V1\Shipping_Methods_Controller::class,
				'tax_classes'           => API\V1\Tax_Classes_Controller::class,
				'order_statuses'        => API\V1\Data_Order_Statuses_Controller::class,
			)
		);

		/**
		 * Filter the wcpos/v2 service pass-through controllers (additive to the
		 * frozen v1 surface — the legacy data controllers stay v1-only).
		 *
		 * Extensions that replace a v1 service through
		 * `woocommerce_pos_rest_api_controllers` must carry their service onto
		 * the v2 surface here with their own pass-through subclass (override
		 * `$namespace = 'wcpos/v2'`), exactly as core does — the v2 map is not
		 * derived from the v1 map, so a v1 replacement alone leaves the v2
		 * twin serving core behavior.
		 *
		 * @since 1.10.0
		 *
		 * @param array $controllers Associative array of v2 service controller class names.
		 */
		$v2_classes = apply_filters(
			'woocommerce_pos_rest_api_v2_controllers',
			array(
				'ping'              => API\V2\Ping::class,
				'echo_probe'        => API\V2\Echo_Probe::class,
				'site'              => API\V2\Site::class,
				'auth'              => API\V2\Auth::class,
				'settings'          => API\V2\Settings::class,
				'cashier'           => API\V2\Cashier::class,
				'templates'         => API\V2\Templates_Controller::class,
				'receipts'          => API\V2\Receipts_Controller::class,
				'print_jobs'        => API\V2\Print_Jobs_Controller::class,
				'stores'            => API\V2\Stores::class,
				'extensions'        => API\V2\Extensions::class,
				'logs'              => API\V2\Logs::class,
				'payment_gateways'  => API\V2\Payment_Gateways::class,
				'gateway_bootstrap' => API\V2\Gateway_Bootstrap_Controller::class,
				'checkout'          => API\V2\Checkout_Controller::class,
				'order_email'       => API\V2\Order_Email_Controller::class,
				'shipping_methods'  => API\V2\Shipping_Methods_Controller::class,
				'tax_classes'       => API\V2\Tax_Classes_Controller::class,
				'order_statuses'    => API\V2\Data_Order_Statuses_Controller::class,
			)
		);
		foreach ( $v2_classes as $key => $class ) {
			$classes[ 'v2-' . $key ] = $class;
		}
		$legacy_classifications = array(
			'auth'       => array( 'public' => array( '/wcpos/v1/auth/test', '/wcpos/v1/auth/refresh' ) ),
			'print_jobs' => array( 'printer_token' => array( '/wcpos/v1/print-jobs/cloudprnt', '/wcpos/v1/print-jobs/epson-sdp' ) ),
			'receipts'   => array( 'permission_error_passthrough' => array( '/wcpos/v1/receipts/' ) ),
		);

		foreach ( $classes as $key => $class ) {
			if ( class_exists( $class ) ) {
				$this->controllers[ $key ] = new $class();
				$this->controllers[ $key ]->register_routes();

				if ( method_exists( $this->controllers[ $key ], 'wcpos_route_classifications' ) ) {
					$this->route_classifier->merge( $this->controllers[ $key ]->wcpos_route_classifications() );
				} elseif ( isset( $legacy_classifications[ $key ] ) ) {
					$this->route_classifier->merge( $legacy_classifications[ $key ] );
				}
			}
		}

		// Sync classifications are independent of feature-gated route registration.
		$this->route_classifier->merge( Sync\Api::route_classifications() );

		// Build route map for use in rest_dispatch_request().
		$rest_server = rest_get_server();

		foreach ( $route_namespaces as $route_namespace ) {
			$all_routes = $rest_server->get_routes( $route_namespace );

			foreach ( $all_routes as $route_pattern => $route_handlers ) {
				foreach ( $route_handlers as $route_handler ) {
					$callback = $route_handler['callback'] ?? null;

					// Extract the controller object from the callback.
					$controller_obj = null;
					if ( \is_array( $callback ) && isset( $callback[0] ) && \is_object( $callback[0] ) ) {
						$controller_obj = $callback[0];
					} elseif ( $callback instanceof \Closure ) {
						// WC 10.5+ RestApiCache wraps callbacks in closures.
						// Use reflection to extract the bound $this.
						$ref            = new \ReflectionFunction( $callback );
						$controller_obj = $ref->getClosureThis();
					}

					if ( ! $controller_obj ) {
						continue;
					}

					// Find which controller key this object belongs to.
					foreach ( $this->controllers as $key => $registered_controller ) {
						if ( $controller_obj === $registered_controller ) {
							$this->route_map[ $route_pattern ] = $key;
							break;
						}
					}
				}
			}
		}
	}

	/**
	 * Check request for any login tokens.
	 *
	 * Runs at priority 20, after other plugins (e.g. third-party JWT plugins at
	 * priority 10) have had a chance to authenticate the user. If another plugin
	 * already returned a valid user ID, we trust it. Otherwise we attempt our own
	 * WCPOS Bearer-token authentication.
	 *
	 * Note: some JWT plugins pass a WP_Error through this filter when they fail to
	 * validate a Bearer token. We treat that the same as false so we can still
	 * authenticate the request with our own JWT.
	 *
	 * Note: this filter may not be called at all when WordPress has already cached
	 * the current user (WooCommerce issue #26847). The rest_authentication_errors
	 * fallback below handles that scenario.
	 *
	 * @param false|int|\WP_Error $user_id User ID if one has been determined, false otherwise.
	 *
	 * @return false|int
	 */
	public function determine_current_user( $user_id ) {
		$this->is_auth_checked = true;

		// Trust a valid user ID set by another plugin (e.g. JWT plugin with its own token).
		// Treat a WP_Error the same as false — another plugin rejected its own token,
		// but we should still attempt authentication with our Bearer token.
		if ( ! empty( $user_id ) && ! is_wp_error( $user_id ) ) {
			return $user_id;
		}

		$result = $this->authenticate( false );
		if ( $result && ! is_wp_error( $result ) ) {
			$this->authenticated_via_wcpos = true;
			return $result;
		}

		// If neither we nor another plugin authenticated the user, return false
		// (not authenticated) rather than a WP_Error from $user_id. WordPress core
		// expects determine_current_user to return false|int, not WP_Error.
		// The JWT plugin's error will surface via rest_authentication_errors instead.
		return is_wp_error( $user_id ) ? false : $user_id;
	}

	/**
	 * Handles two distinct failure modes:
	 *
	 * 1. WooCommerce issue #26847: determine_current_user may not be called when
	 *    WordPress has already cached the current user. We attempt auth here as a
	 *    fallback.
	 *
	 * 2. JWT plugin conflict: a third-party JWT plugin sees our Bearer token, fails
	 *    to validate it with its own secret, and returns a WP_Error via
	 *    rest_authentication_errors at priority 10. We run at priority 50 and attempt
	 *    our own Bearer-token validation. If it succeeds, we clear the stale error —
	 *    our authentication wins. (jwt-authentication-for-wp-rest-api surfaces its
	 *    error through rest_pre_dispatch instead; see clear_third_party_jwt_error().)
	 *
	 * @param mixed $errors Authentication errors.
	 *
	 * @return mixed
	 */
	public function rest_authentication_errors( $errors ) {
		// If there is already an error from a previous filter (e.g. a JWT plugin that
		// rejected our Bearer token), attempt WCPOS authentication before passing it
		// through. This covers the case where determine_current_user was skipped
		// (WC #26847) or where the JWT plugin ran at a higher priority.
		if ( ! empty( $errors ) ) {
			// Only clear errors that originate from JWT authentication plugins. Errors
			// from other mechanisms (maintenance locks, IP restrictions, etc.) should
			// be passed through even when the WCPOS Bearer token is valid.
			if ( $this->is_third_party_jwt_error( $errors ) && $this->ensure_authenticated_via_wcpos() ) {
				return null;
			}

			return $errors;
		}

		// check if determine_current_user has been called.
		if ( ! $this->is_auth_checked && $this->ensure_authenticated_via_wcpos() ) {
			// Authentication hadn't occurred during `determine_current_user`, but our token is valid.
			return true;
		}

		return $errors;
	}

	/**
	 * Clear a third-party JWT plugin's stale error from the dispatch result.
	 *
	 * The plugin jwt-authentication-for-wp-rest-api (verified at 1.5.0) validates every
	 * Bearer token in determine_current_user (priority 10) with its own secret. Ours fails,
	 * so it stores a `jwt_auth_invalid_token` WP_Error and returns the user untouched;
	 * our priority-20 filter then authenticates the request. The plugin later returns
	 * that stored error from rest_pre_dispatch (priority 10, registered at
	 * plugins_loaded), which replaces the dispatch result with a 403.
	 *
	 * Priority 50: after the plugin's callback, and after our own priority-10
	 * permission gate, whose `woocommerce_pos_rest_*` errors must pass through untouched.
	 *
	 * Unlike rest_authentication_errors(), this never switches the current user: the
	 * priority-10 gate and the core-order audit guard have already judged the user in
	 * scope, so the error is cleared only when our token resolves to that same user.
	 *
	 * @param mixed           $result  Dispatch result, or null to not hijack the request.
	 * @param WP_REST_Server  $server  Server instance.
	 * @param WP_REST_Request $request Request used to generate the response.
	 *
	 * @return mixed
	 */
	public function clear_third_party_jwt_error( $result, $server, $request ) {
		if ( ! $this->is_third_party_jwt_error( $result ) ) {
			return $result;
		}

		if ( ! $this->authenticated_via_wcpos ) {
			$user_id = $this->authenticate( false );
			if ( $user_id && ! is_wp_error( $user_id ) && get_current_user_id() === (int) $user_id ) {
				$this->authenticated_via_wcpos = true;
			}
		}

		return $this->authenticated_via_wcpos ? null : $result;
	}

	/**
	 * Whether a value is a WP_Error raised by a third-party JWT plugin (`jwt_auth_*`).
	 *
	 * @param mixed $maybe_error Value to inspect.
	 *
	 * @return bool
	 */
	private function is_third_party_jwt_error( $maybe_error ): bool {
		return is_wp_error( $maybe_error ) && 0 === strpos( $maybe_error->get_error_code(), 'jwt_auth_' );
	}

	/**
	 * Authenticate the request with its WCPOS Bearer token if that hasn't happened yet.
	 *
	 * @return bool True when the request is authenticated via a WCPOS-issued token.
	 */
	private function ensure_authenticated_via_wcpos(): bool {
		if ( ! $this->authenticated_via_wcpos ) {
			$user_id = $this->authenticate( false );
			if ( $user_id && ! is_wp_error( $user_id ) ) {
				wp_set_current_user( $user_id );
				$this->authenticated_via_wcpos = true;
			}
		}

		return $this->authenticated_via_wcpos;
	}

	/**
	 * Extract the Authorization Bearer token from the request.
	 *
	 * @return false|string
	 */
	public function get_auth_header() {
		return Auth::instance()->get_auth_header();
	}

	/**
	 * Adds info to the WP REST API index response.
	 * - UUID
	 * - Version Info.
	 *
	 * @param WP_REST_Response $response Response data.
	 *
	 * @return WP_REST_Response
	 */
	public function rest_index( WP_REST_Response $response ): WP_REST_Response {
		$uuid                               = wcpos_get_site_uuid();
		$response->data['uuid']             = $uuid;
		$response->data['wp_version']       = get_bloginfo( 'version' );
		$response->data['wc_version']       = WC()->version;
		$response->data['wcpos_version']    = VERSION;
		$response->data['use_jwt_as_param'] = SettingsService::instance()->use_jwt_as_param_enabled();

		// Add WCPOS authentication endpoint to the response.
		$response->data['authentication']['wcpos'] = array(
			'endpoints' => array(
				'authorization' => Template_Router::get_auth_url(),
			),
		);

		/**
		 * Remove the routes from the response.
		 *
		 * Some wordpress sites have a huge number of routes, like 2MB of data. It shouldn;t matter, but it seems
		 * to cause issues with the desktop application sometimes. We don't use the routes at the moment, so we
		 * can remove them from the response.
		 */
		$data = $response->get_data();
		unset( $data['routes'] );
		$response->set_data( $data );

		return $response;
	}

	/**
	 * Filters the pre-calculated result of a REST API dispatch request.
	 *
	 * Allow hijacking the request before dispatching by returning a non-empty. The returned value
	 * will be used to serve the request instead.
	 *
	 * @param mixed           $result  Response to replace the requested version with. Can be anything
	 *                                 a normal endpoint can return, or null to not hijack the request.
	 * @param WP_REST_Server  $server  Server instance.
	 * @param WP_REST_Request $request Request used to generate the response.
	 *
	 * @return mixed
	 */
	public function rest_pre_dispatch( $result, $server, $request ) {
		if ( ! $this->route_classifier->in_wcpos_namespace( $request->get_route() ) ) {
			return $result;
		}

		// Marker-gated on purpose (query var or header, NOT the rest_route arm,
		// which matches this namespace by construction): every real POS client,
		// old or new, carries the marker, while unmarked scanner traffic would
		// otherwise inflate the `channel: none` tail this telemetry exists to
		// measure (free#1752). The echo and auth lanes are excluded for the same
		// reason: they are the gate's carve-outs, and a protocol-2 client's
		// connect-time probes deliberately carry no signal — counting them would
		// stamp every modern client with a daily false `none` row.
		if ( 0 === stripos( $request->get_route(), '/wcpos/v2/' )
			&& 1 !== preg_match( '#^/wcpos/v2/(?:echo$|auth(?:/|$))#i', $request->get_route() )
			&& ( wcpos_request( 'query_var' ) || wcpos_request( 'header' ) ) ) {
			try {
				Client_Signal::record( $request );
			} catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Telemetry failures are deliberately ignored.
				// Telemetry must never interrupt a POS request.
			}
		}

		// Latch the till's store scope for the whole request (pro#425). Set
		// unconditionally — including to null — so a scope never leaks from one
		// dispatch into the next. Inner `wc/v3` forwards do not reach this line
		// (they are outside the WCPOS namespace), which is exactly right: the
		// OUTER request owns the scope and stamps it onto the inner ones.
		\WCPOS\WooCommercePOS\Sync\Store_Scope::set_current(
			\WCPOS\WooCommercePOS\Sync\Store_Scope::resolve( $request )
		);

		// CORS preflights carry no credentials (browsers strip Authorization from OPTIONS),
		// so the permission gate must never answer them with 401 — a non-2xx preflight blocks
		// every cross-origin standalone client from the entire namespace. WP core serves
		// OPTIONS with route metadata and Rest_Cors::rest_pre_serve_request adds the CORS headers.
		if ( 'OPTIONS' === $request->get_method() ) {
			return $result;
		}

		// Baseline permission gate: POS endpoints require access_woocommerce_pos; the three
		// sync admin operations instead use their route-level manage_woocommerce check.
		// Exempt public auth, printer-token polling, and authenticated receipt denials that need
		// the receipt-specific error code.
		$route                               = $request->get_route();
		$has_route_specific_permission_error = is_user_logged_in() && $this->route_classifier->is_permission_error_passthrough( $route );
		$is_public_auth_route                = $this->route_classifier->is_public( $route );
		$is_printer_token_route              = $this->route_classifier->is_printer_token( $route );
		$is_sync_admin_route                 = is_user_logged_in() && current_user_can( 'manage_woocommerce' ) && $this->route_classifier->is_admin_op( $route );

		if ( ! $is_public_auth_route && ! $has_route_specific_permission_error && ! $is_printer_token_route && ! $is_sync_admin_route ) {
			if ( ! current_user_can( 'access_woocommerce_pos' ) ) {
				if ( ! is_user_logged_in() ) {
					return new \WP_Error(
						'woocommerce_pos_rest_unauthorized',
						__( 'Authentication required.', 'woocommerce-pos' ),
						array( 'status' => 401 )
					);
				}

				return new \WP_Error(
					'woocommerce_pos_rest_forbidden',
					__( 'You do not have permission to access the POS.', 'woocommerce-pos' ),
					array( 'status' => 403 )
				);
			}
		}

		$max_length = 10000;

		// The sync sub-surface speaks its own wire contract (include = raw id
		// list validated by its controllers); the wcpos_include/exclude rewrite
		// below is a legacy extended-WC-controller workaround and must not
		// mangle sync routes.
		if ( $this->route_classifier->is_rewrite_exempt( $route ) ) {
			return $result;
		}

		// Process 'include' parameter.
		$include = $request->get_param( 'include' );
		if ( $include ) {
			$processed_include = $this->shorten_param_array( $include, $max_length );
			$request->set_param( 'wcpos_include', $processed_include );
			unset( $request['include'] );
		}

		// Process 'exclude' parameter.
		$exclude = $request->get_param( 'exclude' );
		if ( $exclude ) {
			$processed_exclude = $this->shorten_param_array( $exclude, $max_length );
			$request->set_param( 'wcpos_exclude', $processed_exclude );
			unset( $request['exclude'] );
		}

		return $result;
	}

	/**
	 * Add the server pressure bucket to WCPOS REST responses.
	 *
	 * @param mixed           $response REST response.
	 * @param WP_REST_Server  $server   REST server.
	 * @param WP_REST_Request $request  REST request.
	 *
	 * @return mixed
	 */
	public function rest_post_dispatch( $response, $server, $request ) {
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		try {
			if ( ! $response instanceof WP_HTTP_Response || ! $this->route_classifier->in_wcpos_namespace( $request->get_route() ) ) {
				return $response;
			}
			$pressure_bucket = API\V2\Ping::pressure_bucket();
			if ( null !== $pressure_bucket ) {
				$response->header( 'X-WCPOS-Pressure', $pressure_bucket );
			}
		} catch ( \Throwable $e ) {
			return $response;
		}

		return $response;
	}

	/**
	 * Filters the REST API dispatch request result.
	 *
	 * @param mixed           $dispatch_result Dispatch result, will be used if not empty.
	 * @param WP_REST_Request $request         Request used to generate the response.
	 * @param string          $route           Route matched for the request.
	 * @param array           $handler         Route handler used for the request.
	 *
	 * @return mixed
	 */
	public function rest_dispatch_request( $dispatch_result, $request, $route, $handler ) {
		// Only process mapped WCPOS routes.
		if ( ! isset( $this->route_map[ $route ] ) ) {
			return $dispatch_result;
		}

		/*
		 * POS-specific PHP settings to prevent errors in JSON and float weirdness.
		 *
		 * - error_reporting(0) - Turn off error reporting
		 * - ini_set('display_errors', 0) - Turn off error display
		 * - ini_set('precision', 10) - Set the precision of floating point numbers
		 * - ini_set('serialize_precision', 10) - Set the precision of floating point numbers for serialization
		 *
		 * This is to prevent any PHP errors from being displayed in the response.
		 *
		 * The precision settings are to prevent floating point weirdness, eg: stock_quantity 3.6 becomes 3.6000000000000001
		 */
		error_reporting( 0 );
		@ini_set( 'display_errors', '0' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed -- intentionally disabling error display for POS API responses.
		@ini_set( 'precision', '10' );
		@ini_set( 'serialize_precision', '10' );

		$key        = $this->route_map[ $route ];
		$controller = $this->controllers[ $key ] ?? null;

		if ( $controller && method_exists( $controller, 'wcpos_dispatch_request' ) ) {
			return $controller->wcpos_dispatch_request( $dispatch_result, $request, $route, $handler );
		}

		return $dispatch_result;
	}

	/**
	 * Some servers have a limit on the number of include/exclude we can use in a request.
	 * Worst thing is there is often no error message, the request returns an empty response.
	 *
	 * For example, WP Engine has a limit of 1024 characters?
	 * https://wpengine.com/support/using-dev-tools/#Long_Queries_in_wp_db
	 *
	 * @TODO - For long queries, I should find a better solution than this.
	 *
	 * @param array|string $param_value The parameter value.
	 * @param int          $max_length  The maximum length.
	 *
	 * @return array
	 */
	private function shorten_param_array( $param_value, $max_length ) {
		$param_array  = \is_array( $param_value ) ? $param_value : explode( ',', $param_value );
		$param_string = implode( ',', $param_array );

		if ( \strlen( $param_string ) > $max_length ) {
			shuffle( $param_array ); // Shuffle to randomize.

			$new_param_string   = '';
			$random_param_array = array();

			foreach ( $param_array as $id ) {
				if ( \strlen( $new_param_string . $id ) < $max_length ) {
					$new_param_string .= $id . ',';
					$random_param_array[] = $id;
				} else {
					break; // Stop when maximum length is reached.
				}
			}

			return $random_param_array;
		}

		return $param_array;
	}

	/**
	 * Check the Authorization header for a Bearer token.
	 *
	 * @param false|int $user_id User ID if one has been determined, false otherwise.
	 *
	 * @return false|int|\WP_Error
	 */
	private function authenticate( $user_id ) {
		$authenticated_user_id = Auth::instance()->authenticate_request();

		if ( is_wp_error( $authenticated_user_id ) ) {
			return false === $user_id ? $authenticated_user_id : $user_id;
		}

		return false === $authenticated_user_id ? $user_id : $authenticated_user_id;
	}
}

```
