Execute server-side WC validation when clicking button (order creation)

This commit is contained in:
Alex P 2022-10-27 09:30:40 +03:00
parent 3dde305cb4
commit 91636a9dcc
6 changed files with 169 additions and 24 deletions

View file

@ -59,7 +59,9 @@ class CheckoutActionHandler {
);
} else {
errorHandler.clear();
if (data.data.details.length > 0) {
if (data.data.errors.length > 0) {
errorHandler.messages(data.data.errors);
} else if (data.data.details.length > 0) {
errorHandler.message(data.data.details.map(d => `${d.issue} ${d.description}`).join('<br/>'), true);
} else {
errorHandler.message(data.data.message, true);

View file

@ -27,7 +27,7 @@ use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
return array(
'button.client_id' => static function ( ContainerInterface $container ): string {
'button.client_id' => static function ( ContainerInterface $container ): string {
$settings = $container->get( 'wcgateway.settings' );
$client_id = $settings->has( 'client_id' ) ? $settings->get( 'client_id' ) : '';
@ -45,7 +45,7 @@ return array(
return $env->current_environment_is( Environment::SANDBOX ) ?
CONNECT_WOO_SANDBOX_CLIENT_ID : CONNECT_WOO_CLIENT_ID;
},
'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface {
'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface {
$state = $container->get( 'onboarding.state' );
/**
@ -92,16 +92,16 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.url' => static function ( ContainerInterface $container ): string {
'button.url' => static function ( ContainerInterface $container ): string {
return plugins_url(
'/modules/ppcp-button/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'button.request-data' => static function ( ContainerInterface $container ): RequestData {
'button.request-data' => static function ( ContainerInterface $container ): RequestData {
return new RequestData();
},
'button.endpoint.change-cart' => static function ( ContainerInterface $container ): ChangeCartEndpoint {
'button.endpoint.change-cart' => static function ( ContainerInterface $container ): ChangeCartEndpoint {
if ( ! \WC()->cart ) {
throw new RuntimeException( 'cant initialize endpoint at this moment' );
}
@ -113,7 +113,7 @@ return array(
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ChangeCartEndpoint( $cart, $shipping, $request_data, $purchase_unit_factory, $data_store, $logger );
},
'button.endpoint.create-order' => static function ( ContainerInterface $container ): CreateOrderEndpoint {
'button.endpoint.create-order' => static function ( ContainerInterface $container ): CreateOrderEndpoint {
$request_data = $container->get( 'button.request-data' );
$purchase_unit_factory = $container->get( 'api.factory.purchase-unit' );
$order_endpoint = $container->get( 'api.endpoint.order' );
@ -134,10 +134,11 @@ return array(
$early_order_handler,
$registration_needed,
$container->get( 'wcgateway.settings.card_billing_data_mode' ),
$container->get( 'button.early-wc-checkout-validation-enabled' ),
$logger
);
},
'button.helper.early-order-handler' => static function ( ContainerInterface $container ) : EarlyOrderHandler {
'button.helper.early-order-handler' => static function ( ContainerInterface $container ) : EarlyOrderHandler {
$state = $container->get( 'onboarding.state' );
$order_processor = $container->get( 'wcgateway.order-processor' );
@ -145,7 +146,7 @@ return array(
$prefix = $container->get( 'api.prefix' );
return new EarlyOrderHandler( $state, $order_processor, $session_handler, $prefix );
},
'button.endpoint.approve-order' => static function ( ContainerInterface $container ): ApproveOrderEndpoint {
'button.endpoint.approve-order' => static function ( ContainerInterface $container ): ApproveOrderEndpoint {
$request_data = $container->get( 'button.request-data' );
$order_endpoint = $container->get( 'api.endpoint.order' );
$session_handler = $container->get( 'session.handler' );
@ -165,7 +166,7 @@ return array(
$logger
);
},
'button.endpoint.data-client-id' => static function( ContainerInterface $container ) : DataClientIdEndpoint {
'button.endpoint.data-client-id' => static function( ContainerInterface $container ) : DataClientIdEndpoint {
$request_data = $container->get( 'button.request-data' );
$identity_token = $container->get( 'api.endpoint.identity-token' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
@ -175,39 +176,47 @@ return array(
$logger
);
},
'button.endpoint.vault-paypal' => static function( ContainerInterface $container ) : StartPayPalVaultingEndpoint {
'button.endpoint.vault-paypal' => static function( ContainerInterface $container ) : StartPayPalVaultingEndpoint {
return new StartPayPalVaultingEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.payment-token' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure {
'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ThreeDSecure( $logger );
},
'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply {
'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply {
return new MessagesApply(
$container->get( 'api.shop.country' )
);
},
'button.is-logged-in' => static function ( ContainerInterface $container ): bool {
'button.is-logged-in' => static function ( ContainerInterface $container ): bool {
return is_user_logged_in();
},
'button.registration-required' => static function ( ContainerInterface $container ): bool {
'button.registration-required' => static function ( ContainerInterface $container ): bool {
return WC()->checkout()->is_registration_required();
},
'button.current-user-must-register' => static function ( ContainerInterface $container ): bool {
'button.current-user-must-register' => static function ( ContainerInterface $container ): bool {
return ! $container->get( 'button.is-logged-in' ) &&
$container->get( 'button.registration-required' );
},
'button.basic-checkout-validation-enabled' => static function ( ContainerInterface $container ): bool {
'button.basic-checkout-validation-enabled' => static function ( ContainerInterface $container ): bool {
/**
* The filter allowing to disable the basic client-side validation of the checkout form
* when the PayPal button is clicked.
*/
return (bool) apply_filters( 'woocommerce_paypal_payments_basic_checkout_validation_enabled', true );
return (bool) apply_filters( 'woocommerce_paypal_payments_basic_checkout_validation_enabled', false );
},
'button.early-wc-checkout-validation-enabled' => static function ( ContainerInterface $container ): bool {
/**
* The filter allowing to disable the WC validation of the checkout form
* when the PayPal button is clicked.
* The validation is triggered in a non-standard way and may cause issues on some sites.
*/
return (bool) apply_filters( 'woocommerce_paypal_payments_early_wc_checkout_validation_enabled', true );
},
);

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use stdClass;
use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
@ -25,6 +26,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
@ -128,6 +131,13 @@ class CreateOrderEndpoint implements EndpointInterface {
*/
protected $card_billing_data_mode;
/**
* Whether to execute WC validation of the checkout form.
*
* @var bool
*/
protected $early_validation_enabled;
/**
* The logger.
*
@ -148,6 +158,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* @param EarlyOrderHandler $early_order_handler The EarlyOrderHandler object.
* @param bool $registration_needed Whether a new user must be registered during checkout.
* @param string $card_billing_data_mode The value of card_billing_data_mode from the settings.
* @param bool $early_validation_enabled Whether to execute WC validation of the checkout form.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
@ -161,6 +172,7 @@ class CreateOrderEndpoint implements EndpointInterface {
EarlyOrderHandler $early_order_handler,
bool $registration_needed,
string $card_billing_data_mode,
bool $early_validation_enabled,
LoggerInterface $logger
) {
@ -174,6 +186,7 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->early_order_handler = $early_order_handler;
$this->registration_needed = $registration_needed;
$this->card_billing_data_mode = $card_billing_data_mode;
$this->early_validation_enabled = $early_validation_enabled;
$this->logger = $logger;
}
@ -233,8 +246,14 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->set_bn_code( $data );
if ( 'pay-now' === $data['context'] && get_option( 'woocommerce_terms_page_id', '' ) !== '' ) {
$this->validate_paynow_form( $data['form'] );
$form_fields = $data['form'] ?? null;
if ( $this->early_validation_enabled && is_array( $form_fields ) ) {
$this->validate_form( $form_fields );
}
if ( 'pay-now' === $data['context'] && is_array( $form_fields ) && get_option( 'woocommerce_terms_page_id', '' ) !== '' ) {
$this->validate_paynow_form( $form_fields );
}
try {
@ -264,6 +283,13 @@ class CreateOrderEndpoint implements EndpointInterface {
wp_send_json_success( $order->to_array() );
return true;
} catch ( ValidationException $error ) {
wp_send_json_error(
array(
'message' => $error->getMessage(),
'errors' => $error->errors(),
)
);
} catch ( \RuntimeException $error ) {
$this->logger->error( 'Order creation failed: ' . $error->getMessage() );
@ -481,16 +507,33 @@ class CreateOrderEndpoint implements EndpointInterface {
return $payment_method;
}
/**
* Checks whether the form fields are valid.
*
* @param array $form_fields The form fields.
* @throws ValidationException When fields are not valid.
*/
private function validate_form( array $form_fields ): void {
try {
$v = new CheckoutFormValidator();
$v->validate( $form_fields );
} catch ( ValidationException $exception ) {
throw $exception;
} catch ( Throwable $exception ) {
$this->logger->error( "Form validation execution failed. {$exception->getMessage()} {$exception->getFile()}:{$exception->getLine()}" );
}
}
/**
* Checks whether the terms input field is checked.
*
* @param array $form_fields The form fields.
* @throws \RuntimeException When field is not checked.
* @throws ValidationException When field is not checked.
*/
private function validate_paynow_form( array $form_fields ) {
private function validate_paynow_form( array $form_fields ): void {
if ( isset( $form_fields['terms-field'] ) && ! isset( $form_fields['terms'] ) ) {
throw new \RuntimeException(
__( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce-paypal-payments' )
throw new ValidationException(
array( __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce-paypal-payments' ) )
);
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* ValidationException.
*
* @package WooCommerce\PayPalCommerce\Button\Exception
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Exception;
/**
* Class ValidationException
*/
class ValidationException extends RuntimeException {
/**
* The error messages.
*
* @var string[]
*/
protected $errors;
/**
* ValidationException constructor.
*
* @param string[] $errors The validation error messages.
* @param string $message The error message.
*/
public function __construct( array $errors, string $message = '' ) {
$this->errors = $errors;
if ( ! $message ) {
$message = implode( ' ', $errors );
}
parent::__construct( $message );
}
/**
* The error messages.
*
* @return string[]
*/
public function errors(): array {
return $this->errors;
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* Executes WC checkout validation.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Validation;
use WC_Checkout;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WP_Error;
/**
* Class FormValidator
*/
class CheckoutFormValidator extends WC_Checkout {
/**
* Validates the form data.
*
* @param array $data The form data.
* @return void
* @throws ValidationException When validation fails.
*/
public function validate( array $data ) {
$errors = new WP_Error();
if ( isset( $data['terms-field'] ) ) {
// WC checks this field via $_POST https://github.com/woocommerce/woocommerce/issues/35328 .
$_POST['terms-field'] = $data['terms-field'];
}
// It throws some notices when checking fields etc., also from other plugins via hooks.
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@$this->validate_checkout( $data, $errors );
if ( $errors->has_errors() ) {
throw new ValidationException( $errors->get_error_messages() );
}
}
}