WooCommerce Code Reference

ShopperListsNonceCheck.php

Source code

<?php
declare( strict_types = 1 );

namespace Automattic\WooCommerce\StoreApi\Routes\V1;

/**
 * Stopgap CSRF guard for the write-capable shopper-lists routes.
 *
 * Enforces a `wc_store_api` Nonce header on writes and refreshes the
 * client nonce via response headers on every reply. Same shape as the
 * cart's existing flow, scoped to the nonce concern.
 *
 * To be replaced by a reusable Store API-wide nonce trait once that
 * lands on trunk.
 *
 * @internal
 */
trait ShopperListsNonceCheck {
	/**
	 * Nonce action used to sign and verify Store API write requests.
	 *
	 * @var string
	 */
	private static $store_api_nonce_action = 'wc_store_api';

	/**
	 * Override of {@see AbstractRoute::get_response} that enforces the
	 * `wc_store_api` Nonce header on writes and refreshes it on every reply.
	 *
	 * @param \WP_REST_Request $request Request object.
	 * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
	 * @return \WP_REST_Response
	 */
	public function get_response( \WP_REST_Request $request ) {
		if ( $this->is_write_request( $request ) ) {
			$nonce_check = $this->check_store_api_nonce( $request );
			if ( is_wp_error( $nonce_check ) ) {
				return $this->add_nonce_response_headers( $this->error_to_response( $nonce_check ) );
			}
		}

		$response = parent::get_response( $request );

		return $this->add_nonce_response_headers( rest_ensure_response( $response ) );
	}

	/**
	 * Whether the request mutates state. Mirrors `AbstractCartRoute::is_update_request`.
	 *
	 * @param \WP_REST_Request $request Request object.
	 * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
	 * @return bool
	 */
	private function is_write_request( \WP_REST_Request $request ): bool {
		return in_array( $request->get_method(), array( 'POST', 'PUT', 'PATCH', 'DELETE' ), true );
	}

	/**
	 * Verify the `Nonce` request header against the `wc_store_api` action.
	 *
	 * @param \WP_REST_Request $request Request object.
	 * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
	 * @return true|\WP_Error True on success, WP_Error on missing/invalid nonce.
	 */
	private function check_store_api_nonce( \WP_REST_Request $request ) {
		/**
		 * Filters whether to disable the Store API nonce check.
		 *
		 * This filter is documented in src/StoreApi/Routes/V1/AbstractCartRoute.php.
		 *
		 * @since 4.5.0
		 *
		 * @param bool $disable_nonce_check If true, nonce checks will be disabled.
		 */
		if ( apply_filters( 'woocommerce_store_api_disable_nonce_check', false ) ) {
			return true;
		}

		$nonce = $request->get_header( 'Nonce' );
		if ( null === $nonce || '' === $nonce ) {
			return $this->get_route_error_response(
				'woocommerce_rest_missing_nonce',
				__( 'Missing the Nonce header. This endpoint requires a valid nonce.', 'woocommerce' ),
				401
			);
		}

		if ( ! wp_verify_nonce( $nonce, self::$store_api_nonce_action ) ) {
			return $this->get_route_error_response(
				'woocommerce_rest_invalid_nonce',
				__( 'Nonce is invalid.', 'woocommerce' ),
				403
			);
		}

		return true;
	}

	/**
	 * Attach a fresh `wc_store_api` nonce to the response.
	 *
	 * @param \WP_REST_Response $response Response object.
	 * @return \WP_REST_Response
	 */
	private function add_nonce_response_headers( \WP_REST_Response $response ): \WP_REST_Response {
		$response->header( 'Nonce', wp_create_nonce( self::$store_api_nonce_action ) );
		$response->header( 'Nonce-Timestamp', (string) time() );
		$response->header( 'Cache-Control', 'no-store' );

		return $response;
	}
}