diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/actions.js b/plugins/woocommerce-blocks/assets/js/data/cart/actions.js index 3bea4565683..107568521c6 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/actions.js +++ b/plugins/woocommerce-blocks/assets/js/data/cart/actions.js @@ -1,13 +1,14 @@ /** * External dependencies */ -import { apiFetch, select } from '@wordpress/data-controls'; +import { select } from '@wordpress/data-controls'; /** * Internal dependencies */ import { ACTION_TYPES as types } from './action-types'; import { STORE_KEY as CART_STORE_KEY } from './constants'; +import { apiFetchWithHeaders } from '../shared-controls'; /** * Returns an action object used in updating the store with the provided items @@ -69,80 +70,6 @@ export function receiveRemovingCoupon( couponCode ) { }; } -/** - * Applies a coupon code and either invalidates caches, or receives an error if - * the coupon cannot be applied. - * - * @throws Will throw an error if there is an API problem. - * @param {string} couponCode The coupon code to apply to the cart. - */ -export function* applyCoupon( couponCode ) { - yield receiveApplyingCoupon( couponCode ); - - try { - const result = yield apiFetch( { - path: '/wc/store/cart/apply-coupon', - method: 'POST', - data: { - code: couponCode, - }, - cache: 'no-store', - } ); - - if ( result ) { - yield receiveCart( result ); - } - - // Finished handling the coupon. - yield receiveApplyingCoupon( '' ); - } catch ( error ) { - // Store the error message in state. - yield receiveError( error ); - // Re-throw the error. - throw error; - } - - return true; -} - -/** - * Removes a coupon code and either invalidates caches, or receives an error if - * the coupon cannot be removed. - * - * @throws Will throw an error if there is an API problem. - * @param {string} couponCode The coupon code to remove from the cart. - */ -export function* removeCoupon( couponCode ) { - yield receiveRemovingCoupon( couponCode ); - - try { - const result = yield apiFetch( { - path: '/wc/store/cart/remove-coupon', - method: 'POST', - data: { - code: couponCode, - }, - cache: 'no-store', - } ); - - if ( result ) { - yield receiveCart( result ); - } - - // Finished handling the coupon. - yield receiveRemovingCoupon( '' ); - } catch ( error ) { - // Store the error message in state. - yield receiveError( error ); - // Finished handling the coupon. - yield receiveRemovingCoupon( '' ); - // Re-throw the error. - throw error; - } - - return true; -} - /** * Returns an action object for updating a single cart item in the store. * @@ -185,6 +112,85 @@ export function receiveRemovedItem( cartItemKey ) { }; } +/** + * Returns an action object used to track what shipping address are we updating to. + * + * @param {boolean} isResolving if we're loading shipping address or not. + * @return {Object} Object for action. + */ +export function shippingRatesAreResolving( isResolving ) { + return { + type: types.UPDATING_SHIPPING_ADDRESS, + isResolving, + }; +} + +/** + * Applies a coupon code and either invalidates caches, or receives an error if + * the coupon cannot be applied. + * + * @throws Will throw an error if there is an API problem. + * @param {string} couponCode The coupon code to apply to the cart. + */ +export function* applyCoupon( couponCode ) { + yield receiveApplyingCoupon( couponCode ); + + try { + const { response } = yield apiFetchWithHeaders( { + path: '/wc/store/cart/apply-coupon', + method: 'POST', + data: { + code: couponCode, + }, + cache: 'no-store', + } ); + + yield receiveCart( response ); + yield receiveApplyingCoupon( '' ); + } catch ( error ) { + // Store the error message in state. + yield receiveError( error ); + // Re-throw the error. + throw error; + } + + return true; +} + +/** + * Removes a coupon code and either invalidates caches, or receives an error if + * the coupon cannot be removed. + * + * @throws Will throw an error if there is an API problem. + * @param {string} couponCode The coupon code to remove from the cart. + */ +export function* removeCoupon( couponCode ) { + yield receiveRemovingCoupon( couponCode ); + + try { + const { response } = yield apiFetchWithHeaders( { + path: '/wc/store/cart/remove-coupon', + method: 'POST', + data: { + code: couponCode, + }, + cache: 'no-store', + } ); + + yield receiveCart( response ); + yield receiveRemovingCoupon( '' ); + } catch ( error ) { + // Store the error message in state. + yield receiveError( error ); + // Finished handling the coupon. + yield receiveRemovingCoupon( '' ); + // Re-throw the error. + throw error; + } + + return true; +} + /** * Removes specified item from the cart: * - Calls API to remove item. @@ -198,13 +204,13 @@ export function* removeItemFromCart( cartItemKey ) { yield itemQuantityPending( cartItemKey, true ); try { - const cart = yield apiFetch( { + const { response } = yield apiFetchWithHeaders( { path: `/wc/store/cart/remove-item/?key=${ cartItemKey }`, method: 'POST', cache: 'no-store', } ); - yield receiveCart( cart ); + yield receiveCart( response ); } catch ( error ) { yield receiveError( error ); } @@ -228,8 +234,8 @@ export function* changeCartItemQuantity( cartItemKey, quantity ) { return; } try { - const cart = yield apiFetch( { - path: `/wc/store/cart/update-item`, + const { response } = yield apiFetchWithHeaders( { + path: '/wc/store/cart/update-item', method: 'POST', data: { key: cartItemKey, @@ -238,7 +244,7 @@ export function* changeCartItemQuantity( cartItemKey, quantity ) { cache: 'no-store', } ); - yield receiveCart( cart ); + yield receiveCart( response ); } catch ( error ) { yield receiveError( error ); } @@ -252,7 +258,7 @@ export function* changeCartItemQuantity( cartItemKey, quantity ) { */ export function* selectShippingRate( rateId, packageId = 0 ) { try { - const result = yield apiFetch( { + const { response } = yield apiFetchWithHeaders( { path: `/wc/store/cart/select-shipping-rate/${ packageId }`, method: 'POST', data: { @@ -261,9 +267,7 @@ export function* selectShippingRate( rateId, packageId = 0 ) { cache: 'no-store', } ); - if ( result ) { - yield receiveCart( result ); - } + yield receiveCart( response ); } catch ( error ) { yield receiveError( error ); // Re-throw the error. @@ -272,19 +276,6 @@ export function* selectShippingRate( rateId, packageId = 0 ) { return true; } -/** - * Returns an action object used to track what shipping address are we updating to. - * - * @param {boolean} isResolving if we're loading shipping address or not. - * @return {Object} Object for action. - */ -export function shippingRatesAreResolving( isResolving ) { - return { - type: types.UPDATING_SHIPPING_ADDRESS, - isResolving, - }; -} - /** * Applies a coupon code and either invalidates caches, or receives an error if the coupon cannot be applied. @@ -294,16 +285,14 @@ the coupon cannot be applied. export function* updateShippingAddress( address ) { yield shippingRatesAreResolving( true ); try { - const result = yield apiFetch( { + const { response } = yield apiFetchWithHeaders( { path: '/wc/store/cart/update-shipping', method: 'POST', data: address, cache: 'no-store', } ); - if ( result ) { - yield receiveCart( result ); - } + yield receiveCart( response ); } catch ( error ) { yield receiveError( error ); yield shippingRatesAreResolving( false ); diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/index.js b/plugins/woocommerce-blocks/assets/js/data/cart/index.js index 6851c1aa5b3..7b1c636bd92 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/index.js +++ b/plugins/woocommerce-blocks/assets/js/data/cart/index.js @@ -2,7 +2,7 @@ * External dependencies */ import { registerStore } from '@wordpress/data'; -import { controls } from '@wordpress/data-controls'; +import { controls as dataControls } from '@wordpress/data-controls'; /** * Internal dependencies @@ -12,11 +12,12 @@ import * as selectors from './selectors'; import * as actions from './actions'; import * as resolvers from './resolvers'; import reducer from './reducers'; +import { controls } from '../shared-controls'; registerStore( STORE_KEY, { reducer, actions, - controls, + controls: { ...dataControls, ...controls }, selectors, resolvers, } ); diff --git a/plugins/woocommerce-blocks/assets/js/data/collections/index.js b/plugins/woocommerce-blocks/assets/js/data/collections/index.js index da04d5eeea3..346526d6ee2 100644 --- a/plugins/woocommerce-blocks/assets/js/data/collections/index.js +++ b/plugins/woocommerce-blocks/assets/js/data/collections/index.js @@ -12,7 +12,7 @@ import * as selectors from './selectors'; import * as actions from './actions'; import * as resolvers from './resolvers'; import reducer from './reducers'; -import { controls } from './controls'; +import { controls } from '../shared-controls'; registerStore( STORE_KEY, { reducer, diff --git a/plugins/woocommerce-blocks/assets/js/data/collections/resolvers.js b/plugins/woocommerce-blocks/assets/js/data/collections/resolvers.js index fa49835d064..99c34e9450f 100644 --- a/plugins/woocommerce-blocks/assets/js/data/collections/resolvers.js +++ b/plugins/woocommerce-blocks/assets/js/data/collections/resolvers.js @@ -10,7 +10,7 @@ import { addQueryArgs } from '@wordpress/url'; import { receiveCollection, receiveCollectionError } from './actions'; import { STORE_KEY as SCHEMA_STORE_KEY } from '../schema/constants'; import { STORE_KEY, DEFAULT_EMPTY_ARRAY } from './constants'; -import { apiFetchWithHeaders } from './controls'; +import { apiFetchWithHeaders } from '../shared-controls'; /** * Check if the store needs invalidating due to a change in last modified headers. @@ -52,9 +52,9 @@ export function* getCollection( namespace, resourceName, query, ids ) { try { const { - items = DEFAULT_EMPTY_ARRAY, + response = DEFAULT_EMPTY_ARRAY, headers, - } = yield apiFetchWithHeaders( route + queryString ); + } = yield apiFetchWithHeaders( { path: route + queryString } ); if ( headers && headers.get && headers.has( 'last-modified' ) ) { // Do any invalidation before the collection is received to prevent @@ -65,7 +65,7 @@ export function* getCollection( namespace, resourceName, query, ids ) { } yield receiveCollection( namespace, resourceName, queryString, ids, { - items, + items: response, headers, } ); } catch ( error ) { diff --git a/plugins/woocommerce-blocks/assets/js/data/collections/test/resolvers.js b/plugins/woocommerce-blocks/assets/js/data/collections/test/resolvers.js index f1375571474..c6d8ff2cf23 100644 --- a/plugins/woocommerce-blocks/assets/js/data/collections/test/resolvers.js +++ b/plugins/woocommerce-blocks/assets/js/data/collections/test/resolvers.js @@ -10,7 +10,7 @@ import { getCollection, getCollectionHeader } from '../resolvers'; import { receiveCollection } from '../actions'; import { STORE_KEY as SCHEMA_STORE_KEY } from '../../schema/constants'; import { STORE_KEY } from '../constants'; -import { apiFetchWithHeaders } from '../controls'; +import { apiFetchWithHeaders } from '../../shared-controls'; jest.mock( '@wordpress/data-controls' ); @@ -73,7 +73,9 @@ describe( 'getCollection', () => { fulfillment.next(); const { value } = fulfillment.next( 'https://example.org' ); expect( value ).toEqual( - apiFetchWithHeaders( 'https://example.org?foo=bar' ) + apiFetchWithHeaders( { + path: 'https://example.org?foo=bar', + } ) ); } ); @@ -101,7 +103,7 @@ describe( 'getCollection', () => { fulfillment.next(); fulfillment.next( 'https://example.org' ); const { value } = fulfillment.next( { - items: [ '42', 'cheeseburgers' ], + response: [ '42', 'cheeseburgers' ], headers: { foo: 'bar' }, } ); expect( value ).toEqual( diff --git a/plugins/woocommerce-blocks/assets/js/data/collections/controls.js b/plugins/woocommerce-blocks/assets/js/data/shared-controls.js similarity index 64% rename from plugins/woocommerce-blocks/assets/js/data/collections/controls.js rename to plugins/woocommerce-blocks/assets/js/data/shared-controls.js index 352f9c65bda..e84374c9ad9 100644 --- a/plugins/woocommerce-blocks/assets/js/data/collections/controls.js +++ b/plugins/woocommerce-blocks/assets/js/data/shared-controls.js @@ -7,14 +7,14 @@ import triggerFetch from '@wordpress/api-fetch'; * Dispatched a control action for triggering an api fetch call with no parsing. * Typically this would be used in scenarios where headers are needed. * - * @param {string} path The path for the request. + * @param {Object} options The options for the API request. * * @return {Object} The control action descriptor. */ -export const apiFetchWithHeaders = ( path ) => { +export const apiFetchWithHeaders = ( options ) => { return { type: 'API_FETCH_WITH_HEADERS', - path, + options, }; }; @@ -25,12 +25,13 @@ export const apiFetchWithHeaders = ( path ) => { * the controls property of the registration object. */ export const controls = { - API_FETCH_WITH_HEADERS( { path } ) { + API_FETCH_WITH_HEADERS( { options } ) { return new Promise( ( resolve, reject ) => { - triggerFetch( { path, parse: false } ) - .then( ( response ) => { - response.json().then( ( items ) => { - resolve( { items, headers: response.headers } ); + triggerFetch( { ...options, parse: false } ) + .then( ( fetchResponse ) => { + fetchResponse.json().then( ( response ) => { + resolve( { response, headers: fetchResponse.headers } ); + triggerFetch.setNonce( fetchResponse.headers ); } ); } ) .catch( ( error ) => { diff --git a/plugins/woocommerce-blocks/assets/js/settings/blocks/index.js b/plugins/woocommerce-blocks/assets/js/settings/blocks/index.js index c6bf3b521e5..797fe0d1de5 100644 --- a/plugins/woocommerce-blocks/assets/js/settings/blocks/index.js +++ b/plugins/woocommerce-blocks/assets/js/settings/blocks/index.js @@ -1,2 +1,3 @@ +export * from './store-api-nonce'; export * from './constants'; export { ENDPOINTS } from './endpoints'; diff --git a/plugins/woocommerce-blocks/assets/js/settings/blocks/store-api-nonce.js b/plugins/woocommerce-blocks/assets/js/settings/blocks/store-api-nonce.js new file mode 100644 index 00000000000..6aab200b527 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/settings/blocks/store-api-nonce.js @@ -0,0 +1,61 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { getSetting } from '../shared'; + +// Cache for the initial nonce initialized from hydration. +let nonce = getSetting( 'storeApiNonce' ); + +/** + * Returns whether or not this is a non GET wc/store API request. + * + * @param {Object} options Fetch options. + * + * @return {boolean} Returns true if this is a store request. + */ +const isStoreApiGetRequest = ( options ) => { + const url = options.url || options.path; + if ( ! url || ! options.method || options.method === 'GET' ) { + return false; + } + return /wc\/store\//.exec( url ) !== null; +}; + +/** + * Set the current nonce from a header object. + * + * @param {Object} headers Headers object. + */ +const setNonce = ( headers ) => { + const newNonce = headers?.get( 'X-WC-Store-API-Nonce' ); + if ( newNonce ) { + nonce = newNonce; + } +}; + +/** + * Nonce middleware which updates the nonce after a request, if given. + * + * @param {Object} options Fetch options. + * @param {Function} next The next middleware or fetchHandler to call. + * + * @return {*} The evaluated result of the remaining middleware chain. + */ +export const storeNonceMiddleware = ( options, next ) => { + if ( isStoreApiGetRequest( options ) ) { + const existingHeaders = options.headers || {}; + options.headers = { + ...existingHeaders, + 'X-WC-Store-API-Nonce': nonce, + }; + } + return next( options, next ); +}; + +apiFetch.use( storeNonceMiddleware ); +apiFetch.setNonce = setNonce; diff --git a/plugins/woocommerce-blocks/src/Assets.php b/plugins/woocommerce-blocks/src/Assets.php index fe68c122c97..bc3fd9f790e 100644 --- a/plugins/woocommerce-blocks/src/Assets.php +++ b/plugins/woocommerce-blocks/src/Assets.php @@ -141,6 +141,7 @@ class Assets { 'restApiRoutes' => [ '/wc/store' => array_keys( \Automattic\WooCommerce\Blocks\RestApi::get_routes_from_namespace( 'wc/store' ) ), ], + 'storeApiNonce' => wp_create_nonce( 'wc_store_api' ), 'homeUrl' => esc_url( home_url( '/' ) ), 'storePages' => [ 'shop' => self::format_page_resource( $page_ids['shop'] ), diff --git a/plugins/woocommerce-blocks/src/RestApi.php b/plugins/woocommerce-blocks/src/RestApi.php index 2fdb9c0779a..7a5ce4c3b42 100644 --- a/plugins/woocommerce-blocks/src/RestApi.php +++ b/plugins/woocommerce-blocks/src/RestApi.php @@ -21,6 +21,7 @@ class RestApi { add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ), 10 ); add_action( 'rest_api_init', array( '\Automattic\WooCommerce\Blocks\RestApi\StoreApi\RoutesController', 'register_routes' ), 10 ); add_filter( 'rest_authentication_errors', array( __CLASS__, 'maybe_init_cart_session' ), 1 ); + add_filter( 'rest_authentication_errors', array( __CLASS__, 'store_api_authentication' ) ); } /** @@ -55,6 +56,37 @@ class RestApi { return isset( $response_data['routes'] ) ? $response_data['routes'] : null; } + /** + * Check if is request to the Store API. + * + * @return bool + */ + protected static function is_request_to_store_api() { + if ( empty( $_SERVER['REQUEST_URI'] ) ) { + return false; + } + + $rest_prefix = trailingslashit( rest_get_url_prefix() ); + $request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ); + + return false !== strpos( $request_uri, $rest_prefix . 'wc/store' ); + } + + /** + * The Store API does not require authentication. + * + * @param \WP_Error|mixed $result Error from another authentication handler, null if we should handle it, or another value if not. + * @return \WP_Error|null|bool + */ + public static function store_api_authentication( $result ) { + // Pass through errors from other authentication methods used before this one. + if ( ! empty( $result ) || ! self::is_request_to_store_api() ) { + return $result; + } + + return true; + } + /** * If we're making a cart request, we may need to load some additional classes from WC Core so we're ready to deal with requests. * diff --git a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Routes/AbstractRoute.php b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Routes/AbstractRoute.php index 4afce63f30a..928bdbe443c 100644 --- a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Routes/AbstractRoute.php +++ b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Routes/AbstractRoute.php @@ -50,6 +50,9 @@ abstract class AbstractRoute implements RouteInterface { public function get_response( \WP_REST_Request $request ) { $response = null; try { + if ( 'GET' !== $request->get_method() ) { + $this->check_nonce( $request ); + } switch ( $request->get_method() ) { case 'POST': $response = $this->get_route_post_response( $request ); @@ -65,6 +68,9 @@ abstract class AbstractRoute implements RouteInterface { $response = $this->get_route_response( $request ); break; } + if ( 'GET' !== $request->get_method() && ! is_wp_error( $response ) ) { + $response->header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); + } } catch ( RouteException $error ) { $response = new \WP_Error( $error->getErrorCode(), $error->getMessage(), [ 'status' => $error->getCode() ] ); } catch ( Exception $error ) { @@ -73,6 +79,30 @@ abstract class AbstractRoute implements RouteInterface { return $response; } + /** + * For non-GET endpoints, require and validate a nonce to prevent CSRF attacks. + * + * Nonces will mismatch if the logged in session cookie is different! If using a client to test, set this cookie + * to match the logged in cookie in your browser. + * + * @throws RouteException On error. + * + * @param \WP_REST_Request $request Request object. + */ + protected function check_nonce( \WP_REST_Request $request ) { + $nonce = $request->get_header( 'X-WC-Store-API-Nonce' ); + + if ( null === $nonce ) { + throw new RouteException( 'woocommerce_rest_missing_nonce', __( 'Missing the X-WC-Store-API-Nonce header. This endpoint requires a valid nonce.', 'woo-gutenberg-products-block' ), 403 ); + } + + $valid_nonce = wp_verify_nonce( $nonce, 'wc_store_api' ); + + if ( ! $valid_nonce ) { + throw new RouteException( 'woocommerce_rest_invalid_nonce', __( 'X-WC-Store-API-Nonce is invalid.', 'woo-gutenberg-products-block' ), 403 ); + } + } + /** * Get route response for GET requests. * diff --git a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/Cart.php b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/Cart.php index cb30baaee0c..c43f959e47e 100644 --- a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/Cart.php +++ b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/Cart.php @@ -98,6 +98,7 @@ class Cart extends TestCase { public function test_remove_bad_cart_item() { // Test removing a bad cart item - should return 404. $request = new WP_REST_Request( 'POST', '/wc/store/cart/remove-item' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'key' => 'bad_item_key_123', @@ -116,6 +117,7 @@ class Cart extends TestCase { public function test_remove_cart_item() { // Test removing a valid cart item - should return updated cart. $request = new WP_REST_Request( 'POST', '/wc/store/cart/remove-item' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'key' => $this->keys[0], @@ -143,6 +145,7 @@ class Cart extends TestCase { */ public function test_update_item() { $request = new WP_REST_Request( 'POST', '/wc/store/cart/update-item' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'key' => $this->keys[0], @@ -163,6 +166,7 @@ class Cart extends TestCase { */ public function test_update_shipping() { $request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'country' => 'US', @@ -188,6 +192,7 @@ class Cart extends TestCase { public function test_get_items_address_validation() { // US address. $request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'address_1' => 'Test address 1', @@ -211,6 +216,7 @@ class Cart extends TestCase { // Address with empty country. $request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'country' => '' @@ -223,6 +229,7 @@ class Cart extends TestCase { // Address with invalid country. $request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'country' => 'ZZZZZZZZ' @@ -235,6 +242,7 @@ class Cart extends TestCase { // US address with named state. $request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'state' =>'Alabama', @@ -250,6 +258,7 @@ class Cart extends TestCase { // US address with invalid state. $request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'state' =>'ZZZZZZZZ', @@ -270,6 +279,7 @@ class Cart extends TestCase { wc()->cart->remove_coupon( $this->coupon->get_code() ); $request = new WP_REST_Request( 'POST', '/wc/store/cart/apply-coupon' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'code' => $this->coupon->get_code(), @@ -283,6 +293,7 @@ class Cart extends TestCase { // Test coupons with different case. $newcoupon = CouponHelper::create_coupon( 'testCoupon' ); $request = new WP_REST_Request( 'POST', '/wc/store/cart/apply-coupon' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'code' => 'testCoupon', @@ -295,6 +306,7 @@ class Cart extends TestCase { // Test coupons with special chars in the code. $newcoupon = CouponHelper::create_coupon( '$5 off' ); $request = new WP_REST_Request( 'POST', '/wc/store/cart/apply-coupon' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'code' => '$5 off', @@ -311,6 +323,7 @@ class Cart extends TestCase { public function test_remove_coupon() { // Invalid coupon. $request = new WP_REST_Request( 'POST', '/wc/store/cart/remove-coupon' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'code' => 'doesnotexist', @@ -322,6 +335,7 @@ class Cart extends TestCase { // Applied coupon. $request = new WP_REST_Request( 'POST', '/wc/store/cart/remove-coupon' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'code' => $this->coupon->get_code(), diff --git a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/CartCoupons.php b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/CartCoupons.php index e857b1e0063..1ff66eb6459 100644 --- a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/CartCoupons.php +++ b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/CartCoupons.php @@ -74,6 +74,7 @@ class CartCoupons extends TestCase { wc()->cart->remove_coupons(); $request = new WP_REST_Request( 'POST', '/wc/store/cart/coupons' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'code' => $this->coupon->get_code(), @@ -93,6 +94,7 @@ class CartCoupons extends TestCase { wc()->cart->remove_coupons(); $request = new WP_REST_Request( 'POST', '/wc/store/cart/coupons' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'code' => 'IDONOTEXIST', @@ -109,6 +111,7 @@ class CartCoupons extends TestCase { */ public function test_delete_item() { $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/coupons/' . $this->coupon->get_code() ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $response = $this->server->dispatch( $request ); $data = $response->get_data(); @@ -116,12 +119,14 @@ class CartCoupons extends TestCase { $this->assertEmpty( $data ); $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/coupons/' . $this->coupon->get_code() ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $response = $this->server->dispatch( $request ); $data = $response->get_data(); $this->assertEquals( 404, $response->get_status() ); $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/coupons/i-do-not-exist' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $response = $this->server->dispatch( $request ); $data = $response->get_data(); @@ -133,6 +138,7 @@ class CartCoupons extends TestCase { */ public function test_delete_items() { $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/coupons' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $response = $this->server->dispatch( $request ); $data = $response->get_data(); diff --git a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/CartCreateOrder.php b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/CartCreateOrder.php index 7e5ddddf275..0fac75cdb07 100644 --- a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/CartCreateOrder.php +++ b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/CartCreateOrder.php @@ -63,6 +63,7 @@ class CartCreateOrder extends TestCase { */ public function test_create_item() { $request = new WP_REST_Request( 'POST', '/wc/store/cart/create-order' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_param( 'billing_address', [ diff --git a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/CartItems.php b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/CartItems.php index 3d9c65d8faa..b4f3f7e6c77 100644 --- a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/CartItems.php +++ b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Routes/CartItems.php @@ -92,6 +92,7 @@ class CartItems extends TestCase { $this->assertEquals( '2000', $data['totals']->line_total ); $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/items/XXX815416f775098fe977004015c6193' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $response = $this->server->dispatch( $request ); $data = $response->get_data(); @@ -105,6 +106,7 @@ class CartItems extends TestCase { wc_empty_cart(); $request = new WP_REST_Request( 'POST', '/wc/store/cart/items' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'id' => $this->products[0]->get_id(), @@ -137,6 +139,7 @@ class CartItems extends TestCase { $invalid_product->save(); $request = new WP_REST_Request( 'POST', '/wc/store/cart/items' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'id' => $invalid_product->get_id(), @@ -153,6 +156,7 @@ class CartItems extends TestCase { */ public function test_update_item() { $request = new WP_REST_Request( 'PUT', '/wc/store/cart/items/' . $this->keys[0] ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $request->set_body_params( array( 'quantity' => '10', @@ -170,6 +174,7 @@ class CartItems extends TestCase { */ public function test_delete_item() { $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/items/' . $this->keys[0] ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $response = $this->server->dispatch( $request ); $data = $response->get_data(); @@ -177,6 +182,7 @@ class CartItems extends TestCase { $this->assertEmpty( $data ); $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/items/' . $this->keys[0] ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $response = $this->server->dispatch( $request ); $data = $response->get_data(); @@ -188,6 +194,7 @@ class CartItems extends TestCase { */ public function test_delete_items() { $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/items' ); + $request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) ); $response = $this->server->dispatch( $request ); $data = $response->get_data();