diff --git a/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md b/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md index 08d66036a7c..9aa12b425b3 100644 --- a/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md +++ b/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md @@ -43,6 +43,7 @@ The majority of our feature flagging is blocks, this is a list of them: - Product Rating Counter ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/src/BlockTypesController.php#L229) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/bin/webpack-entries.js#L71-L73)) - ⚛️ Add to cart ([JS flag](https://github.com/woocommerce/woocommerce-blocks/blob/dfd2902bd8a247b5d048577db6753c5e901fc60f/assets/js/atomic/blocks/product-elements/add-to-cart/index.ts#L26-L29)). - Order Route ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/b4a9dc9334f82c09f533b0f88c947b5c34e4e546/src/StoreApi/RoutesController.php#L65-L67)) +- Checkout Order Route ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/add/an-endpoint-for-process-pay-for-order-orders/src/StoreApi/RoutesController.php#L67)) ## Features behind flags diff --git a/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/CartUpdateCustomer.php b/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/CartUpdateCustomer.php index bceeaff7e32..a9201b33320 100644 --- a/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/CartUpdateCustomer.php +++ b/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/CartUpdateCustomer.php @@ -7,7 +7,7 @@ use Automattic\WooCommerce\StoreApi\Utilities\ValidationUtils; /** * CartUpdateCustomer class. * - * Updates the customer billing and shipping address and returns an updated cart--things such as taxes may be recalculated. + * Updates the customer billing and shipping addresses, recalculates the cart totals, and returns an updated cart. */ class CartUpdateCustomer extends AbstractCartRoute { use DraftOrderTrait; diff --git a/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/Checkout.php b/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/Checkout.php index 6251b9c707d..3c7942367ed 100644 --- a/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/Checkout.php +++ b/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/Checkout.php @@ -1,7 +1,6 @@ add_response_headers( $response ); } - /** - * Prepare a single item for response. Handles setting the status based on the payment result. - * - * @param mixed $item Item to format to schema. - * @param \WP_REST_Request $request Request object. - * @return \WP_REST_Response $response Response data. - */ - public function prepare_item_for_response( $item, \WP_REST_Request $request ) { - $response = parent::prepare_item_for_response( $item, $request ); - $status_codes = [ - 'success' => 200, - 'pending' => 202, - 'failure' => 400, - 'error' => 500, - ]; - - if ( isset( $item->payment_result ) && $item->payment_result instanceof PaymentResult ) { - $response->set_status( $status_codes[ $item->payment_result->status ] ?? 200 ); - } - - return $response; - } - /** * Convert the cart into a new draft order, or update an existing draft order, and return an updated cart response. * @@ -466,133 +444,6 @@ class Checkout extends AbstractCartRoute { $customer->save(); } - /** - * Update the current order using the posted values from the request. - * - * @param \WP_REST_Request $request Full details about the request. - */ - private function update_order_from_request( \WP_REST_Request $request ) { - $this->order->set_customer_note( $request['customer_note'] ?? '' ); - $this->order->set_payment_method( $this->get_request_payment_method_id( $request ) ); - $this->order->set_payment_method_title( $this->get_request_payment_method_title( $request ) ); - - wc_do_deprecated_action( - '__experimental_woocommerce_blocks_checkout_update_order_from_request', - array( - $this->order, - $request, - ), - '6.3.0', - 'woocommerce_store_api_checkout_update_order_from_request', - 'This action was deprecated in WooCommerce Blocks version 6.3.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.' - ); - - wc_do_deprecated_action( - 'woocommerce_blocks_checkout_update_order_from_request', - array( - $this->order, - $request, - ), - '7.2.0', - 'woocommerce_store_api_checkout_update_order_from_request', - 'This action was deprecated in WooCommerce Blocks version 7.2.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.' - ); - - /** - * Fires when the Checkout Block/Store API updates an order's from the API request data. - * - * This hook gives extensions the chance to update orders based on the data in the request. This can be used in - * conjunction with the ExtendSchema class to post custom data and then process it. - * - * @since 7.2.0 - * - * @param \WC_Order $order Order object. - * @param \WP_REST_Request $request Full details about the request. - */ - do_action( 'woocommerce_store_api_checkout_update_order_from_request', $this->order, $request ); - - $this->order->save(); - } - - /** - * For orders which do not require payment, just update status. - * - * @param \WP_REST_Request $request Request object. - * @param PaymentResult $payment_result Payment result object. - */ - private function process_without_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { - // Transition the order to pending, and then completed. This ensures transactional emails fire for pending_to_complete events. - $this->order->update_status( 'pending' ); - $this->order->payment_complete(); - - // Mark the payment as successful. - $payment_result->set_status( 'success' ); - $payment_result->set_redirect_url( $this->order->get_checkout_order_received_url() ); - } - - /** - * Fires an action hook instructing active payment gateways to process the payment for an order and provide a result. - * - * @throws RouteException On error. - * - * @param \WP_REST_Request $request Request object. - * @param PaymentResult $payment_result Payment result object. - */ - private function process_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { - try { - // Transition the order to pending before making payment. - $this->order->update_status( 'pending' ); - - // Prepare the payment context object to pass through payment hooks. - $context = new PaymentContext(); - $context->set_payment_method( $this->get_request_payment_method_id( $request ) ); - $context->set_payment_data( $this->get_request_payment_data( $request ) ); - $context->set_order( $this->order ); - - /** - * Process payment with context. - * - * @hook woocommerce_rest_checkout_process_payment_with_context - * - * @throws \Exception If there is an error taking payment, an \Exception object can be thrown with an error message. - * - * @param PaymentContext $context Holds context for the payment, including order ID and payment method. - * @param PaymentResult $payment_result Result object for the transaction. - */ - do_action_ref_array( 'woocommerce_rest_checkout_process_payment_with_context', [ $context, &$payment_result ] ); - - if ( ! $payment_result instanceof PaymentResult ) { - throw new RouteException( 'woocommerce_rest_checkout_invalid_payment_result', __( 'Invalid payment result received from payment method.', 'woo-gutenberg-products-block' ), 500 ); - } - } catch ( \Exception $e ) { - throw new RouteException( 'woocommerce_rest_checkout_process_payment_error', $e->getMessage(), 402 ); - } - } - - /** - * Gets the chosen payment method ID from the request. - * - * @throws RouteException On error. - * @param \WP_REST_Request $request Request object. - * @return string - */ - private function get_request_payment_method_id( \WP_REST_Request $request ) { - $payment_method = $this->get_request_payment_method( $request ); - return is_null( $payment_method ) ? '' : $payment_method->id; - } - - /** - * Gets the chosen payment method title from the request. - * - * @throws RouteException On error. - * @param \WP_REST_Request $request Request object. - * @return string - */ - private function get_request_payment_method_title( \WP_REST_Request $request ) { - $payment_method = $this->get_request_payment_method( $request ); - return is_null( $payment_method ) ? '' : $payment_method->get_title(); - } - /** * Gets the chosen payment method from the request. * @@ -633,26 +484,6 @@ class Checkout extends AbstractCartRoute { return $available_gateways[ $request_payment_method ]; } - /** - * Gets and formats payment request data. - * - * @param \WP_REST_Request $request Request object. - * @return array - */ - private function get_request_payment_data( \WP_REST_Request $request ) { - static $payment_data = []; - if ( ! empty( $payment_data ) ) { - return $payment_data; - } - if ( ! empty( $request['payment_data'] ) ) { - foreach ( $request['payment_data'] as $data ) { - $payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] ); - } - } - - return $payment_data; - } - /** * Order processing relating to customer account. * diff --git a/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/CheckoutOrder.php b/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/CheckoutOrder.php new file mode 100644 index 00000000000..be08217b16b --- /dev/null +++ b/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/CheckoutOrder.php @@ -0,0 +1,266 @@ +[\d]+)'; + } + + /** + * Get method arguments for this REST route. + * + * @return array An array of endpoints. + */ + public function get_args() { + return [ + [ + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'get_response' ], + 'permission_callback' => [ $this, 'is_authorized' ], + 'args' => array_merge( + [ + 'payment_data' => [ + 'description' => __( 'Data to pass through to the payment method when processing payment.', 'woo-gutenberg-products-block' ), + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'key' => [ + 'type' => 'string', + ], + 'value' => [ + 'type' => [ 'string', 'boolean' ], + ], + ], + ], + ], + ], + $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ) + ), + ], + 'schema' => [ $this->schema, 'get_public_item_schema' ], + 'allow_batch' => [ 'v1' => true ], + ]; + } + + /** + * Process an order. + * + * 1. Process Request + * 2. Process Customer + * 3. Validate Order + * 4. Process Payment + * + * @throws RouteException On error. + * @throws InvalidStockLevelsInCartException On error. + * + * @param \WP_REST_Request $request Request object. + * + * @return \WP_REST_Response + */ + protected function get_route_post_response( \WP_REST_Request $request ) { + $order_id = absint( $request['id'] ); + $this->order = wc_get_order( $order_id ); + + if ( $this->order->get_status() !== 'pending' && $this->order->get_status() !== 'failed' ) { + return new \WP_Error( + 'invalid_order_update_status', + __( 'This order cannot be paid for.', 'woo-gutenberg-products-block' ) + ); + } + + /** + * Process request data. + * + * Note: Customer data is persisted from the request first so that OrderController::update_addresses_from_cart + * uses the up to date customer address. + */ + $this->update_billing_address( $request ); + $this->update_order_from_request( $request ); + + /** + * Process customer data. + * + * Update order with customer details, and sign up a user account as necessary. + */ + $this->process_customer( $request ); + + /** + * Validate order. + * + * This logic ensures the order is valid before payment is attempted. + */ + $this->order_controller->validate_order_before_payment( $this->order ); + + /** + * Fires before an order is processed by the Checkout Block/Store API. + * + * This hook informs extensions that $order has completed processing and is ready for payment. + * + * This is similar to existing core hook woocommerce_checkout_order_processed. We're using a new action: + * - To keep the interface focused (only pass $order, not passing request data). + * - This also explicitly indicates these orders are from checkout block/StoreAPI. + * + * @since 7.2.0 + * + * @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3238 + * @example See docs/examples/checkout-order-processed.md + + * @param \WC_Order $order Order object. + */ + do_action( 'woocommerce_store_api_checkout_order_processed', $this->order ); + + /** + * Process the payment and return the results. + */ + $payment_result = new PaymentResult(); + + if ( $this->order->needs_payment() ) { + $this->process_payment( $request, $payment_result ); + } else { + $this->process_without_payment( $request, $payment_result ); + } + + return $this->prepare_item_for_response( + (object) [ + 'order' => wc_get_order( $this->order ), + 'payment_result' => $payment_result, + ], + $request + ); + } + + /** + * Updates the current customer session using data from the request (e.g. address data). + * + * Address session data is synced to the order itself later on by OrderController::update_order_from_cart() + * + * @param \WP_REST_Request $request Full details about the request. + */ + private function update_billing_address( \WP_REST_Request $request ) { + $customer = wc()->customer; + $billing = $request['billing_address']; + $shipping = $request['shipping_address']; + + // Billing address is a required field. + foreach ( $billing as $key => $value ) { + if ( is_callable( [ $customer, "set_billing_$key" ] ) ) { + $customer->{"set_billing_$key"}( $value ); + } + } + + // If shipping address (optional field) was not provided, set it to the given billing address (required field). + $shipping_address_values = $shipping ?? $billing; + + foreach ( $shipping_address_values as $key => $value ) { + if ( is_callable( [ $customer, "set_shipping_$key" ] ) ) { + $customer->{"set_shipping_$key"}( $value ); + } elseif ( 'phone' === $key ) { + $customer->update_meta_data( 'shipping_phone', $value ); + } + } + + /** + * Fires when the Checkout Block/Store API updates a customer from the API request data. + * + * @since 8.2.0 + * + * @param \WC_Customer $customer Customer object. + * @param \WP_REST_Request $request Full details about the request. + */ + do_action( 'woocommerce_store_api_checkout_update_customer_from_request', $customer, $request ); + + $customer->save(); + + $this->order->set_billing_address( $billing ); + $this->order->set_shipping_address( $shipping ); + $this->order->save(); + $this->order->calculate_totals(); + } + + /** + * Gets the chosen payment method from the request. + * + * @throws RouteException On error. + * @param \WP_REST_Request $request Request object. + * @return \WC_Payment_Gateway|null + */ + private function get_request_payment_method( \WP_REST_Request $request ) { + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + $request_payment_method = wc_clean( wp_unslash( $request['payment_method'] ?? '' ) ); + $requires_payment_method = $this->order->needs_payment(); + + if ( empty( $request_payment_method ) ) { + if ( $requires_payment_method ) { + throw new RouteException( + 'woocommerce_rest_checkout_missing_payment_method', + __( 'No payment method provided.', 'woo-gutenberg-products-block' ), + 400 + ); + } + return null; + } + + if ( ! isset( $available_gateways[ $request_payment_method ] ) ) { + throw new RouteException( + 'woocommerce_rest_checkout_payment_method_disabled', + sprintf( + // Translators: %s Payment method ID. + __( 'The %s payment gateway is not available.', 'woo-gutenberg-products-block' ), + esc_html( $request_payment_method ) + ), + 400 + ); + } + + return $available_gateways[ $request_payment_method ]; + } + + /** + * Updates the order with user details (e.g. address). + * + * @throws RouteException API error object with error details. + * @param \WP_REST_Request $request Request object. + */ + private function process_customer( \WP_REST_Request $request ) { + $this->order_controller->sync_customer_data_with_order( $this->order ); + } +} diff --git a/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/Order.php b/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/Order.php index 3a6d04e7850..3fc42d32395 100644 --- a/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/Order.php +++ b/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/Order.php @@ -5,11 +5,14 @@ use Automattic\WooCommerce\StoreApi\SchemaController; use Automattic\WooCommerce\StoreApi\Schemas\v1\AbstractSchema; use Automattic\WooCommerce\StoreApi\Utilities\OrderController; use Automattic\WooCommerce\StoreApi\Exceptions\RouteException; +use Automattic\WooCommerce\StoreApi\Utilities\OrderAuthorizationTrait; /** * Order class. */ class Order extends AbstractRoute { + use OrderAuthorizationTrait; + /** * The route identifier. * @@ -71,42 +74,6 @@ class Order extends AbstractRoute { ]; } - /** - * Check if authorized to get the order. - * - * @throws RouteException If the order is not found or the order key is invalid. - * - * @param \WP_REST_Request $request Request object. - * @return boolean|WP_Error - */ - public function is_authorized( \WP_REST_Request $request ) { - $order_id = absint( $request['id'] ); - $order_key = wc_clean( wp_unslash( $request->get_param( 'key' ) ) ); - $billing_email = wc_clean( wp_unslash( $request->get_param( 'billing_email' ) ) ); - - try { - // In this context, pay_for_order capability checks that the current user ID matches the customer ID stored - // within the order, or if the order was placed by a guest. - // See https://github.com/woocommerce/woocommerce/blob/abcedbefe02f9e89122771100c42ff588da3e8e0/plugins/woocommerce/includes/wc-user-functions.php#L458. - if ( ! current_user_can( 'pay_for_order', $order_id ) ) { - throw new RouteException( 'woocommerce_rest_invalid_user', __( 'This order belongs to a different customer. Please log in to the correct account.', 'woo-gutenberg-products-block' ), 403 ); - } - - if ( get_current_user_id() === 0 ) { - $this->order_controller->validate_order_key( $order_id, $order_key ); - $this->order_controller->validate_billing_email( $order_id, $billing_email ); - } - } catch ( RouteException $error ) { - return new \WP_Error( - $error->getErrorCode(), - $error->getMessage(), - array( 'status' => $error->getCode() ) - ); - } - - return true; - } - /** * Handle the request and return a valid response for this endpoint. * @@ -115,6 +82,6 @@ class Order extends AbstractRoute { */ protected function get_route_response( \WP_REST_Request $request ) { $order_id = absint( $request['id'] ); - return rest_ensure_response( $this->schema->get_item_response( $this->order_controller->get_order( $order_id ) ) ); + return rest_ensure_response( $this->schema->get_item_response( wc_get_order( $order_id ) ) ); } } diff --git a/plugins/woocommerce-blocks/src/StoreApi/RoutesController.php b/plugins/woocommerce-blocks/src/StoreApi/RoutesController.php index bf94a73c417..4e7c01346bf 100644 --- a/plugins/woocommerce-blocks/src/StoreApi/RoutesController.php +++ b/plugins/woocommerce-blocks/src/StoreApi/RoutesController.php @@ -63,7 +63,8 @@ class RoutesController { ]; if ( Package::is_experimental_build() ) { - $this->routes['v1'][ Routes\V1\Order::IDENTIFIER ] = Routes\V1\Order::class; + $this->routes['v1'][ Routes\V1\Order::IDENTIFIER ] = Routes\V1\Order::class; + $this->routes['v1'][ Routes\V1\CheckoutOrder::IDENTIFIER ] = Routes\V1\CheckoutOrder::class; } } diff --git a/plugins/woocommerce-blocks/src/StoreApi/SchemaController.php b/plugins/woocommerce-blocks/src/StoreApi/SchemaController.php index 478b99e3599..0454c4d416f 100644 --- a/plugins/woocommerce-blocks/src/StoreApi/SchemaController.php +++ b/plugins/woocommerce-blocks/src/StoreApi/SchemaController.php @@ -43,6 +43,7 @@ class SchemaController { Schemas\V1\CartItemSchema::IDENTIFIER => Schemas\V1\CartItemSchema::class, Schemas\V1\CartSchema::IDENTIFIER => Schemas\V1\CartSchema::class, Schemas\V1\CartExtensionsSchema::IDENTIFIER => Schemas\V1\CartExtensionsSchema::class, + Schemas\V1\CheckoutOrderSchema::IDENTIFIER => Schemas\V1\CheckoutOrderSchema::class, Schemas\V1\CheckoutSchema::IDENTIFIER => Schemas\V1\CheckoutSchema::class, Schemas\V1\OrderItemSchema::IDENTIFIER => Schemas\V1\OrderItemSchema::class, Schemas\V1\OrderCouponSchema::IDENTIFIER => Schemas\V1\OrderCouponSchema::class, diff --git a/plugins/woocommerce-blocks/src/StoreApi/Schemas/V1/CheckoutOrderSchema.php b/plugins/woocommerce-blocks/src/StoreApi/Schemas/V1/CheckoutOrderSchema.php new file mode 100644 index 00000000000..6c362ed6664 --- /dev/null +++ b/plugins/woocommerce-blocks/src/StoreApi/Schemas/V1/CheckoutOrderSchema.php @@ -0,0 +1,37 @@ + $this->get_item_responses_from_schema( $this->coupon_schema, $order->get_items( 'coupon' ) ), 'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $order->get_items( 'fee' ) ), 'totals' => (object) $this->prepare_currency_response( $this->get_totals( $order ) ), - 'shipping_address' => (object) $this->shipping_address_schema->get_item_response( new \WC_Customer( $order->get_customer_id() ) ), - 'billing_address' => (object) $this->billing_address_schema->get_item_response( new \WC_Customer( $order->get_customer_id() ) ), + 'shipping_address' => (object) $this->shipping_address_schema->get_item_response( $order ), + 'billing_address' => (object) $this->billing_address_schema->get_item_response( $order ), 'needs_payment' => $order->needs_payment(), 'needs_shipping' => $order->needs_shipping_address(), 'payment_requirements' => $this->extend->get_payment_requirements(), diff --git a/plugins/woocommerce-blocks/src/StoreApi/Utilities/CheckoutTrait.php b/plugins/woocommerce-blocks/src/StoreApi/Utilities/CheckoutTrait.php new file mode 100644 index 00000000000..24e41fc5b3f --- /dev/null +++ b/plugins/woocommerce-blocks/src/StoreApi/Utilities/CheckoutTrait.php @@ -0,0 +1,182 @@ + 200, + 'pending' => 202, + 'failure' => 400, + 'error' => 500, + ]; + + if ( isset( $item->payment_result ) && $item->payment_result instanceof PaymentResult ) { + $response->set_status( $status_codes[ $item->payment_result->status ] ?? 200 ); + } + + return $response; + } + + /** + * For orders which do not require payment, just update status. + * + * @param \WP_REST_Request $request Request object. + * @param PaymentResult $payment_result Payment result object. + */ + private function process_without_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { + // Transition the order to pending, and then completed. This ensures transactional emails fire for pending_to_complete events. + $this->order->update_status( 'pending' ); + $this->order->payment_complete(); + + // Mark the payment as successful. + $payment_result->set_status( 'success' ); + $payment_result->set_redirect_url( $this->order->get_checkout_order_received_url() ); + } + + /** + * Fires an action hook instructing active payment gateways to process the payment for an order and provide a result. + * + * @throws RouteException On error. + * + * @param \WP_REST_Request $request Request object. + * @param PaymentResult $payment_result Payment result object. + */ + private function process_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { + try { + // Transition the order to pending before making payment. + $this->order->update_status( 'pending' ); + + // Prepare the payment context object to pass through payment hooks. + $context = new PaymentContext(); + $context->set_payment_method( $this->get_request_payment_method_id( $request ) ); + $context->set_payment_data( $this->get_request_payment_data( $request ) ); + $context->set_order( $this->order ); + + /** + * Process payment with context. + * + * @hook woocommerce_rest_checkout_process_payment_with_context + * + * @throws \Exception If there is an error taking payment, an \Exception object can be thrown with an error message. + * + * @param PaymentContext $context Holds context for the payment, including order ID and payment method. + * @param PaymentResult $payment_result Result object for the transaction. + */ + do_action_ref_array( 'woocommerce_rest_checkout_process_payment_with_context', [ $context, &$payment_result ] ); + + if ( ! $payment_result instanceof PaymentResult ) { + throw new RouteException( 'woocommerce_rest_checkout_invalid_payment_result', __( 'Invalid payment result received from payment method.', 'woo-gutenberg-products-block' ), 500 ); + } + } catch ( \Exception $e ) { + throw new RouteException( 'woocommerce_rest_checkout_process_payment_error', $e->getMessage(), 402 ); + } + } + + /** + * Gets the chosen payment method ID from the request. + * + * @throws RouteException On error. + * @param \WP_REST_Request $request Request object. + * @return string + */ + private function get_request_payment_method_id( \WP_REST_Request $request ) { + $payment_method = $this->get_request_payment_method( $request ); + return is_null( $payment_method ) ? '' : $payment_method->id; + } + + /** + * Gets and formats payment request data. + * + * @param \WP_REST_Request $request Request object. + * @return array + */ + private function get_request_payment_data( \WP_REST_Request $request ) { + static $payment_data = []; + if ( ! empty( $payment_data ) ) { + return $payment_data; + } + if ( ! empty( $request['payment_data'] ) ) { + foreach ( $request['payment_data'] as $data ) { + $payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] ); + } + } + + return $payment_data; + } + + /** + * Update the current order using the posted values from the request. + * + * @param \WP_REST_Request $request Full details about the request. + */ + private function update_order_from_request( \WP_REST_Request $request ) { + $this->order->set_customer_note( $request['customer_note'] ?? '' ); + $this->order->set_payment_method( $this->get_request_payment_method_id( $request ) ); + $this->order->set_payment_method_title( $this->get_request_payment_method_title( $request ) ); + + wc_do_deprecated_action( + '__experimental_woocommerce_blocks_checkout_update_order_from_request', + array( + $this->order, + $request, + ), + '6.3.0', + 'woocommerce_store_api_checkout_update_order_from_request', + 'This action was deprecated in WooCommerce Blocks version 6.3.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.' + ); + + wc_do_deprecated_action( + 'woocommerce_blocks_checkout_update_order_from_request', + array( + $this->order, + $request, + ), + '7.2.0', + 'woocommerce_store_api_checkout_update_order_from_request', + 'This action was deprecated in WooCommerce Blocks version 7.2.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.' + ); + + /** + * Fires when the Checkout Block/Store API updates an order's from the API request data. + * + * This hook gives extensions the chance to update orders based on the data in the request. This can be used in + * conjunction with the ExtendSchema class to post custom data and then process it. + * + * @since 7.2.0 + * + * @param \WC_Order $order Order object. + * @param \WP_REST_Request $request Full details about the request. + */ + do_action( 'woocommerce_store_api_checkout_update_order_from_request', $this->order, $request ); + + $this->order->save(); + } + + /** + * Gets the chosen payment method title from the request. + * + * @throws RouteException On error. + * @param \WP_REST_Request $request Request object. + * @return string + */ + private function get_request_payment_method_title( \WP_REST_Request $request ) { + $payment_method = $this->get_request_payment_method( $request ); + return is_null( $payment_method ) ? '' : $payment_method->get_title(); + } +} diff --git a/plugins/woocommerce-blocks/src/StoreApi/Utilities/OrderAuthorizationTrait.php b/plugins/woocommerce-blocks/src/StoreApi/Utilities/OrderAuthorizationTrait.php new file mode 100644 index 00000000000..94040603f7f --- /dev/null +++ b/plugins/woocommerce-blocks/src/StoreApi/Utilities/OrderAuthorizationTrait.php @@ -0,0 +1,63 @@ +get_param( 'key' ) ) ); + $billing_email = sanitize_text_field( wp_unslash( $request->get_param( 'billing_email' ) ) ); + + try { + // In this context, pay_for_order capability checks that the current user ID matches the customer ID stored + // within the order, or if the order was placed by a guest. + // See https://github.com/woocommerce/woocommerce/blob/abcedbefe02f9e89122771100c42ff588da3e8e0/plugins/woocommerce/includes/wc-user-functions.php#L458. + if ( ! current_user_can( 'pay_for_order', $order_id ) ) { + throw new RouteException( 'woocommerce_rest_invalid_user', __( 'This order belongs to a different customer.', 'woo-gutenberg-products-block' ), 403 ); + } + if ( get_current_user_id() === 0 ) { + $this->order_controller->validate_order_key( $order_id, $order_key ); + $this->validate_billing_email_matches_order( $order_id, $billing_email ); + } + } catch ( RouteException $error ) { + return new \WP_Error( + $error->getErrorCode(), + $error->getMessage(), + array( 'status' => $error->getCode() ) + ); + } + + return true; + } + + /** + * Validate a given billing email against an existing order. + * + * @throws RouteException Exception if invalid data is detected. + * @param integer $order_id Order ID. + * @param string $billing_email Billing email. + */ + public function validate_billing_email_matches_order( $order_id, $billing_email ) { + $order = wc_get_order( $order_id ); + + if ( ! $order || ! $billing_email || $order->get_billing_email() !== $billing_email ) { + throw new RouteException( 'woocommerce_rest_invalid_billing_email', __( 'Invalid billing email provided.', 'woo-gutenberg-products-block' ), 401 ); + } + } + +} diff --git a/plugins/woocommerce-blocks/src/StoreApi/Utilities/OrderController.php b/plugins/woocommerce-blocks/src/StoreApi/Utilities/OrderController.php index 64afc22f8ca..7f07e2a56f6 100644 --- a/plugins/woocommerce-blocks/src/StoreApi/Utilities/OrderController.php +++ b/plugins/woocommerce-blocks/src/StoreApi/Utilities/OrderController.php @@ -38,18 +38,6 @@ class OrderController { return $order; } - /** - * Get order. - * - * @throws RouteException Exception if invalid data is detected. - * - * @param integer $order_id Order ID. - * @return \WC_Order A new order object. - */ - public function get_order( $order_id ) { - return wc_get_order( $order_id ); - } - /** * Update an order using data from the current cart. * @@ -481,28 +469,13 @@ class OrderController { * @param string $order_key Order key. */ public function validate_order_key( $order_id, $order_key ) { - $order = $this->get_order( $order_id ); + $order = wc_get_order( $order_id ); if ( ! $order || ! $order_key || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) { throw new RouteException( 'woocommerce_rest_invalid_order', __( 'Invalid order ID or key provided.', 'woo-gutenberg-products-block' ), 401 ); } } - /** - * Validate a given billing email against an existing order. - * - * @throws RouteException Exception if invalid data is detected. - * @param integer $order_id Order ID. - * @param string $billing_email Billing email. - */ - public function validate_billing_email( $order_id, $billing_email ) { - $order = $this->get_order( $order_id ); - - if ( ! $order || ! $billing_email || $order->get_billing_email() !== $billing_email ) { - throw new RouteException( 'woocommerce_rest_invalid_billing_email', __( 'Invalid billing email provided.', 'woo-gutenberg-products-block' ), 401 ); - } - } - /** * Get errors for order stock on failed orders. * @@ -510,7 +483,7 @@ class OrderController { * @param integer $order_id Order ID. */ public function get_failed_order_stock_error( $order_id ) { - $order = $this->get_order( $order_id ); + $order = wc_get_order( $order_id ); // Ensure order items are still stocked if paying for a failed order. Pending orders do not need this check because stock is held. if ( ! $order->has_status( wc_get_is_pending_statuses() ) ) {