[Experiment] Additional field extensible sanitisation and validation handling (#44463)
* Fix field saving in account area
* Check for error notices in core form handler
* Tidy up additional field saving in api
* sanitize_additional_fields does not expect request
* Validate fields with extra context
* Revert "Tidy up additional field saving in api"
This reverts commit 872c8f4afb
.
* Tidy update_customer_from_request
* validate_field docs
* Validation and sanitization hooks
* Address schema validates fields and address location
* Validate locations
* Frontend validation
* Remove empty error response
* Document account details hook
* field_key
* Improve validation routine
* Changelog
* Swap key and value in woocommerce_blocks_validate_additional_field hook
* woocommerce_blocks_validate_location_X_fields
* Validation and sanitization callbacks
* Update try catch blocks
This commit is contained in:
parent
cea1c10122
commit
51a9da9f2c
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
This is behind a feature flag.
|
|
@ -353,12 +353,20 @@ class WC_Form_Handler {
|
|||
$customer->save();
|
||||
}
|
||||
|
||||
wc_add_notice( __( 'Account details changed successfully.', 'woocommerce' ) );
|
||||
|
||||
/**
|
||||
* Hook: woocommerce_save_account_details.
|
||||
*
|
||||
* @since 3.6.0
|
||||
* @param int $user_id User ID being saved.
|
||||
*/
|
||||
do_action( 'woocommerce_save_account_details', $user->ID );
|
||||
|
||||
wp_safe_redirect( wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) ) );
|
||||
exit;
|
||||
// Notices are checked here so that if something created a notice during the save hooks above, the redirect will not happen.
|
||||
if ( 0 === wc_notice_count( 'error' ) ) {
|
||||
wc_add_notice( __( 'Account details changed successfully.', 'woocommerce' ) );
|
||||
wp_safe_redirect( wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) ) );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -901,7 +909,7 @@ class WC_Form_Handler {
|
|||
$quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$variations = array();
|
||||
|
||||
$product = wc_get_product( $product_id );
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
foreach ( $_REQUEST as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Blocks\Domain\Services;
|
|||
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
|
||||
use WC_Customer;
|
||||
use WC_Order;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Service class managing checkout fields and its related extensibility points.
|
||||
|
@ -248,12 +249,43 @@ class CheckoutFields {
|
|||
return array_merge( $keys, array( self::BILLING_FIELDS_KEY, self::SHIPPING_FIELDS_KEY, self::ADDITIONAL_FIELDS_KEY ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* If a field does not declare a sanitization callback, this is the default sanitization callback.
|
||||
*
|
||||
* @param mixed $value Value to sanitize.
|
||||
* @param array $field Field data.
|
||||
* @return mixed
|
||||
*/
|
||||
public function default_sanitize_callback( $value, $field ) {
|
||||
return wc_clean( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* If a field does not declare a validation callback, this is the default validation callback.
|
||||
*
|
||||
* @param mixed $value Value to sanitize.
|
||||
* @param array $field Field data.
|
||||
* @return WP_Error|void If there is a validation error, return an WP_Error object.
|
||||
*/
|
||||
public function default_validate_callback( $value, $field ) {
|
||||
if ( ! empty( $field['required'] ) && empty( $value ) ) {
|
||||
return new WP_Error(
|
||||
'woocommerce_blocks_checkout_field_required',
|
||||
sprintf(
|
||||
// translators: %s is field key.
|
||||
__( 'The field %s is required.', 'woocommerce' ),
|
||||
$field['id']
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an additional field for Checkout.
|
||||
*
|
||||
* @param array $options The field options.
|
||||
*
|
||||
* @return \WP_Error|void True if the field was registered, a WP_Error otherwise.
|
||||
* @return WP_Error|void True if the field was registered, a WP_Error otherwise.
|
||||
*/
|
||||
public function register_checkout_field( $options ) {
|
||||
// Check the options and show warnings if they're not supplied. Return early if an error that would prevent registration is encountered.
|
||||
|
@ -278,6 +310,8 @@ class CheckoutFields {
|
|||
'required' => false,
|
||||
'attributes' => array(),
|
||||
'show_in_order_confirmation' => true,
|
||||
'sanitize_callback' => array( $this, 'default_sanitize_callback' ),
|
||||
'validate_callback' => array( $this, 'default_validate_callback' ),
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -367,6 +401,18 @@ class CheckoutFields {
|
|||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $options['sanitize_callback'] ) && ! is_callable( $options['sanitize_callback'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The sanitize_callback must be a valid callback.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! empty( $options['validate_callback'] ) && ! is_callable( $options['validate_callback'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The validate_callback must be a valid callback.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -537,73 +583,97 @@ class CheckoutFields {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validate an additional field against any custom validation rules. The result should be a WP_Error or true.
|
||||
* Sanitize an additional field against any custom sanitization rules.
|
||||
*
|
||||
* @param string $key The key of the field.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @param \WP_REST_Request $request The current API Request.
|
||||
* @param string|null $address_type The type of address (billing, shipping, or null if the field is a contact/additional field).
|
||||
* @since 8.7.0
|
||||
|
||||
* @param string $field_key The key of the field.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @return mixed
|
||||
*/
|
||||
public function sanitize_field( $field_key, $field_value ) {
|
||||
try {
|
||||
$field = $this->additional_fields[ $field_key ] ?? null;
|
||||
|
||||
if ( $field ) {
|
||||
$field_value = call_user_func( $field['sanitize_callback'], $field_value, $field );
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow custom sanitization of an additional field.
|
||||
*
|
||||
* @param mixed $field_value The value of the field being sanitized.
|
||||
* @param string $field_key Key of the field being sanitized.
|
||||
*
|
||||
* @since 8.7.0
|
||||
*/
|
||||
return apply_filters( 'woocommerce_blocks_sanitize_additional_field', $field_value, $field_key );
|
||||
|
||||
} catch ( \Throwable $e ) {
|
||||
// One of the filters errored so skip it. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'Field sanitization for %s encountered an error. %s',
|
||||
esc_html( $field_key ),
|
||||
esc_html( $e->getMessage() )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
|
||||
return $field_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an additional field against any custom validation rules.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param string $field_key The key of the field.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @return WP_Error
|
||||
*/
|
||||
public function validate_field( $key, $field_value, $request, $address_type = null ) {
|
||||
public function validate_field( $field_key, $field_value ) {
|
||||
$errors = new WP_Error();
|
||||
|
||||
$error = new \WP_Error();
|
||||
try {
|
||||
/**
|
||||
* Filter the result of validating an additional field.
|
||||
*
|
||||
* @param \WP_Error $error A WP_Error that extensions may add errors to.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @param \WP_REST_Request $request The current API Request.
|
||||
* @param string|null $address_type The type of address (billing, shipping, or null if the field is a contact/additional field).
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
$filtered_result = apply_filters( 'woocommerce_blocks_validate_additional_field_' . $key, $error, $field_value, $request, $address_type );
|
||||
$field = $this->additional_fields[ $field_key ] ?? null;
|
||||
|
||||
if ( $error !== $filtered_result ) {
|
||||
if ( $field ) {
|
||||
$validation = call_user_func( $field['validate_callback'], $field_value, $field );
|
||||
|
||||
// Different WP_Error was returned. This would remove errors from other filters. Skip filtering and allow the order to place without validating this field.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s encountered an error. One of the filters returned a new WP_Error. Filters should use the same WP_Error passed to the filter and use the WP_Error->add function to add errors. The field will not have any custom validation applied to it.',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key ),
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
$errors->merge_from( $validation );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass an error object to allow validation of an additional field.
|
||||
*
|
||||
* @param WP_Error $errors A WP_Error object that extensions may add errors to.
|
||||
* @param string $field_key Key of the field being sanitized.
|
||||
* @param mixed $field_value The value of the field being validated.
|
||||
*
|
||||
* @since 8.7.0
|
||||
*/
|
||||
do_action( 'woocommerce_blocks_validate_additional_field', $errors, $field_key, $field_value );
|
||||
|
||||
} catch ( \Throwable $e ) {
|
||||
|
||||
// One of the filters errored so skip them and validate the field. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s encountered an error. The field will not have any custom validation applied to it. %s',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key ),
|
||||
'Field validation for %s encountered an error. %s',
|
||||
esc_html( $field_key ),
|
||||
esc_html( $e->getMessage() )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
|
||||
return new \WP_Error();
|
||||
}
|
||||
|
||||
if ( is_wp_error( $filtered_result ) ) {
|
||||
return $filtered_result;
|
||||
}
|
||||
|
||||
// If the filters didn't return a valid value, ignore them and return an empty WP_Error. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s did not return a valid value. The field will not have any custom validation applied to it.',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
return new \WP_Error();
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -648,24 +718,6 @@ class CheckoutFields {
|
|||
return $this->fields_locations['additional'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of fields definitions only meant for order.
|
||||
*
|
||||
* @return array An array of fields definitions.
|
||||
*/
|
||||
public function get_order_only_fields() {
|
||||
// For now, all contact fields are order only fields, along with additional fields.
|
||||
$order_fields_keys = array_merge( $this->get_contact_fields_keys(), $this->get_additional_fields_keys() );
|
||||
|
||||
return array_filter(
|
||||
$this->get_additional_fields(),
|
||||
function( $key ) use ( $order_fields_keys ) {
|
||||
return in_array( $key, $order_fields_keys, true );
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of fields for a given group.
|
||||
*
|
||||
|
@ -688,17 +740,60 @@ class CheckoutFields {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validates a field value for a given group.
|
||||
* Validates a set of fields for a given location against custom validation rules.
|
||||
*
|
||||
* @param array $fields Array of key value pairs of field values to validate.
|
||||
* @param string $location The location being validated (address|contact|additional).
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
* @return WP_Error
|
||||
*/
|
||||
public function validate_fields_for_location( $fields, $location, $group = '' ) {
|
||||
$errors = new WP_Error();
|
||||
|
||||
try {
|
||||
/**
|
||||
* Pass an error object to allow validation of an additional field.
|
||||
*
|
||||
* @param WP_Error $errors A WP_Error object that extensions may add errors to.
|
||||
* @param mixed $fields List of fields (key value pairs) in this location.
|
||||
* @param string $group The group of this location (shipping|billing|'').
|
||||
*
|
||||
* @since 8.7.0
|
||||
*/
|
||||
do_action( 'woocommerce_blocks_validate_location_' . $location . '_fields', $errors, $fields, $group );
|
||||
|
||||
} catch ( \Throwable $e ) {
|
||||
|
||||
// One of the filters errored so skip them. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The action %s encountered an error. The field location %s may not have any custom validation applied to it. %s',
|
||||
esc_html( 'woocommerce_blocks_validate_' . $location . '_fields' ),
|
||||
esc_html( $location ),
|
||||
esc_html( $e->getMessage() )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a field to check it belongs to the given location and is valid according to its registration.
|
||||
*
|
||||
* This does not apply any custom validation rules on the value.
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param string $location The location to validate the field for (address|contact|additional).
|
||||
*
|
||||
* @return true|\WP_Error True if the field is valid, a WP_Error otherwise.
|
||||
* @return true|WP_Error True if the field is valid, a WP_Error otherwise.
|
||||
*/
|
||||
public function validate_field_for_location( $key, $value, $location ) {
|
||||
if ( ! $this->is_field( $key ) ) {
|
||||
return new \WP_Error(
|
||||
return new WP_Error(
|
||||
'woocommerce_blocks_checkout_field_invalid',
|
||||
\sprintf(
|
||||
// translators: % is field key.
|
||||
|
@ -709,7 +804,7 @@ class CheckoutFields {
|
|||
}
|
||||
|
||||
if ( ! in_array( $key, $this->fields_locations[ $location ], true ) ) {
|
||||
return new \WP_Error(
|
||||
return new WP_Error(
|
||||
'woocommerce_blocks_checkout_field_invalid_location',
|
||||
\sprintf(
|
||||
// translators: %1$s is field key, %2$s location.
|
||||
|
@ -722,7 +817,7 @@ class CheckoutFields {
|
|||
|
||||
$field = $this->additional_fields[ $key ];
|
||||
if ( ! empty( $field['required'] ) && empty( $value ) ) {
|
||||
return new \WP_Error(
|
||||
return new WP_Error(
|
||||
'woocommerce_blocks_checkout_field_required',
|
||||
\sprintf(
|
||||
// translators: %s is field key.
|
||||
|
@ -986,6 +1081,28 @@ class CheckoutFields {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* From a set of fields, returns only the ones for a given location.
|
||||
*
|
||||
* @param array $fields The fields to filter.
|
||||
* @param string $location The location to validate the field for (address|contact|additional).
|
||||
* @return array The filtered fields.
|
||||
*/
|
||||
public function filter_fields_for_location( $fields, $location ) {
|
||||
return array_filter(
|
||||
$fields,
|
||||
function( $key ) use ( $location ) {
|
||||
if ( 0 === strpos( $key, '/billing/' ) ) {
|
||||
$key = str_replace( '/billing/', '', $key );
|
||||
} elseif ( 0 === strpos( $key, '/shipping/' ) ) {
|
||||
$key = str_replace( '/shipping/', '', $key );
|
||||
}
|
||||
return $this->is_field( $key ) && $this->get_field_location( $key ) === $location;
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter fields for order confirmation.
|
||||
*
|
||||
|
|
|
@ -37,12 +37,14 @@ class CheckoutFieldsFrontend {
|
|||
// Show custom checkout fields on the My Account page.
|
||||
add_action( 'woocommerce_my_account_after_my_address', array( $this, 'render_address_fields' ), 10, 1 );
|
||||
|
||||
// Field editing in my account area.
|
||||
// Edit account form under my account (for contact details).
|
||||
add_filter( 'woocommerce_save_account_details_required_fields', array( $this, 'edit_account_form_required_fields' ), 10, 1 );
|
||||
add_filter( 'woocommerce_edit_account_form_fields', array( $this, 'edit_account_form_fields' ), 10, 1 );
|
||||
add_action( 'woocommerce_save_account_details', array( $this, 'save_account_form_fields' ), 10, 1 );
|
||||
|
||||
// Edit address form under my account.
|
||||
add_filter( 'woocommerce_address_to_edit', array( $this, 'edit_address_fields' ), 10, 2 );
|
||||
add_action( 'woocommerce_after_save_address_validation', array( $this, 'save_address_fields' ), 10, 2 );
|
||||
add_action( 'woocommerce_after_save_address_validation', array( $this, 'save_address_fields' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -177,20 +179,46 @@ class CheckoutFieldsFrontend {
|
|||
/**
|
||||
* Validates and saves additional address fields to the customer object on the My Account page.
|
||||
*
|
||||
* Customer is not provided by this hook so we handle save here.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*/
|
||||
public function save_account_form_fields( $user_id ) {
|
||||
$customer = new WC_Customer( $user_id );
|
||||
$fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
|
||||
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
foreach ( $fields as $field_key => $field ) {
|
||||
if ( ! isset( $_POST[ $field_key ] ) ) {
|
||||
$customer = new WC_Customer( $user_id );
|
||||
$additional_fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
|
||||
$field_values = array();
|
||||
|
||||
foreach ( $additional_fields as $key => $field ) {
|
||||
if ( ! isset( $_POST[ $key ] ) ) {
|
||||
continue;
|
||||
}
|
||||
$this->checkout_fields_controller->persist_field_for_customer( $field_key, wc_clean( wp_unslash( $_POST[ $field_key ] ) ), $customer );
|
||||
|
||||
$field_value = $this->checkout_fields_controller->sanitize_field( $key, wc_clean( wp_unslash( $_POST[ $key ] ) ) );
|
||||
$validation = $this->checkout_fields_controller->validate_field( $key, $field_value );
|
||||
|
||||
if ( is_wp_error( $validation ) && $validation->has_errors() ) {
|
||||
wc_add_notice( $validation->get_error_message(), 'error' );
|
||||
continue;
|
||||
}
|
||||
|
||||
$field_values[ $key ] = $field_value;
|
||||
}
|
||||
|
||||
// Persist individual additional fields to customer.
|
||||
foreach ( $field_values as $key => $value ) {
|
||||
$this->checkout_fields_controller->persist_field_for_customer( $key, $value, $customer );
|
||||
}
|
||||
|
||||
// Validate all fields for this location.
|
||||
$location_validation = $this->checkout_fields_controller->validate_fields_for_location( $field_values, 'contact' );
|
||||
|
||||
if ( is_wp_error( $location_validation ) && $location_validation->has_errors() ) {
|
||||
wc_add_notice( $location_validation->get_error_message(), 'error' );
|
||||
}
|
||||
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
$customer->save();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -205,7 +233,7 @@ class CheckoutFieldsFrontend {
|
|||
$fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
|
||||
|
||||
foreach ( $fields as $key => $field ) {
|
||||
$field_key = 'billing' === $address_type ? '/billing/' . $key : '/shipping/' . $key;
|
||||
$field_key = "/{$address_type}/{$key}";
|
||||
$address[ $field_key ] = $field;
|
||||
$address[ $field_key ]['value'] = $this->checkout_fields_controller->get_field_from_customer( $key, $customer, $address_type );
|
||||
|
||||
|
@ -223,33 +251,49 @@ class CheckoutFieldsFrontend {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validates and saves additional address fields to the customer object on the My Account page.
|
||||
* For the My Account page, save address fields. This uses the Store API endpoint for saving addresses so
|
||||
* extensibility hooks are consistent across the codebase.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
* @param string $address_type Type of address (billing or shipping).
|
||||
* The caller saves the customer object if there are no errors. Nonces are checked before this method executes.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
* @param string $address_type Type of address (billing or shipping).
|
||||
* @param array $address Address fields.
|
||||
* @param WC_Customer $customer Customer object.
|
||||
*/
|
||||
public function save_address_fields( $user_id, $address_type ) {
|
||||
$customer = new WC_Customer( $user_id );
|
||||
$fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
|
||||
|
||||
// Nonces are checked before the action is fired that this function hooks into.
|
||||
public function save_address_fields( $user_id, $address_type, $address, $customer ) {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
foreach ( $fields as $key => $field ) {
|
||||
$field_key = 'billing' === $address_type ? '/billing/' . $key : '/shipping/' . $key;
|
||||
$additional_fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
|
||||
$field_values = array();
|
||||
|
||||
if ( ! isset( $_POST[ $field_key ] ) ) {
|
||||
foreach ( $additional_fields as $key => $field ) {
|
||||
$post_key = "/{$address_type}/{$key}";
|
||||
|
||||
if ( ! isset( $_POST[ $post_key ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = wc_clean( wp_unslash( $_POST[ $field_key ] ) );
|
||||
$field_value = $this->checkout_fields_controller->sanitize_field( $key, wc_clean( wp_unslash( $_POST[ $post_key ] ) ) );
|
||||
$validation = $this->checkout_fields_controller->validate_field( $key, $field_value );
|
||||
|
||||
if ( ! empty( $field['required'] ) && empty( $value ) ) {
|
||||
// translators: %s field label.
|
||||
wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), $field['label'] ), 'error' );
|
||||
if ( is_wp_error( $validation ) && $validation->has_errors() ) {
|
||||
wc_add_notice( $validation->get_error_message(), 'error' );
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->checkout_fields_controller->persist_field_for_customer( $field_key, $value, $customer );
|
||||
$field_values[ $key ] = $field_value;
|
||||
}
|
||||
|
||||
// Persist individual additional fields to customer.
|
||||
foreach ( $field_values as $key => $value ) {
|
||||
$this->checkout_fields_controller->persist_field_for_customer( "/{$address_type}/{$key}", $value, $customer );
|
||||
}
|
||||
|
||||
// Validate all fields for this location.
|
||||
$location_validation = $this->checkout_fields_controller->validate_fields_for_location( array_merge( $address, $field_values ), 'address', $address_type );
|
||||
|
||||
if ( is_wp_error( $location_validation ) && $location_validation->has_errors() ) {
|
||||
wc_add_notice( $location_validation->get_error_message(), 'error' );
|
||||
}
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
|
|
@ -424,8 +424,9 @@ class Checkout extends AbstractCartRoute {
|
|||
|
||||
// Billing address is a required field.
|
||||
foreach ( $request['billing_address'] as $key => $value ) {
|
||||
if ( is_callable( [ $customer, "set_billing_$key" ] ) ) {
|
||||
$customer->{"set_billing_$key"}( $value );
|
||||
$callback = "set_billing_$key";
|
||||
if ( is_callable( [ $customer, $callback ] ) ) {
|
||||
$customer->$callback( $value );
|
||||
} elseif ( $this->additional_fields_controller->is_field( $key ) ) {
|
||||
$this->additional_fields_controller->persist_field_for_customer( "/billing/$key", $value, $customer );
|
||||
}
|
||||
|
@ -435,10 +436,9 @@ class Checkout extends AbstractCartRoute {
|
|||
$shipping_address_values = $request['shipping_address'] ?? $request['billing_address'];
|
||||
|
||||
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 );
|
||||
$callback = "set_shipping_$key";
|
||||
if ( is_callable( [ $customer, $callback ] ) ) {
|
||||
$customer->$callback( $value );
|
||||
} elseif ( $this->additional_fields_controller->is_field( $key ) ) {
|
||||
$this->additional_fields_controller->persist_field_for_customer( "/shipping/$key", $value, $customer );
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
|||
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
|
||||
use Automattic\WooCommerce\StoreApi\SchemaController;
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
|
||||
/**
|
||||
* AddressSchema class.
|
||||
*
|
||||
|
@ -132,10 +133,12 @@ abstract class AbstractAddressSchema extends AbstractSchema {
|
|||
$carry[ $key ] = $address['postcode'] ? wc_format_postcode( sanitize_text_field( wp_unslash( $address['postcode'] ) ), $address['country'] ) : '';
|
||||
break;
|
||||
default:
|
||||
$rest_sanitized = rest_sanitize_value_from_schema( wp_unslash( $address[ $key ] ), $field_schema[ $key ], $key );
|
||||
$carry[ $key ] = $rest_sanitized;
|
||||
$carry[ $key ] = rest_sanitize_value_from_schema( wp_unslash( $address[ $key ] ), $field_schema[ $key ], $key );
|
||||
break;
|
||||
}
|
||||
if ( $this->additional_fields_controller->is_field( $key ) ) {
|
||||
$carry[ $key ] = $this->additional_fields_controller->sanitize_field( $key, $carry[ $key ] );
|
||||
}
|
||||
return $carry;
|
||||
},
|
||||
[]
|
||||
|
@ -247,8 +250,7 @@ abstract class AbstractAddressSchema extends AbstractSchema {
|
|||
// Check if a field is in the list of additional fields then validate the value against the custom validation rules defined for it.
|
||||
// Skip additional validation if the schema validation failed.
|
||||
if ( true === $result && in_array( $key, $additional_keys, true ) ) {
|
||||
$address_type = 'shipping_address' === $this->title ? 'shipping' : 'billing';
|
||||
$result = $this->additional_fields_controller->validate_field( $key, $address[ $key ], $request, $address_type );
|
||||
$result = $this->additional_fields_controller->validate_field( $key, $address[ $key ] );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $result ) && $result->has_errors() ) {
|
||||
|
@ -256,6 +258,12 @@ abstract class AbstractAddressSchema extends AbstractSchema {
|
|||
}
|
||||
}
|
||||
|
||||
$result = $this->additional_fields_controller->validate_fields_for_location( $address, 'address', 'billing_address' === $this->title ? 'billing' : 'shipping' );
|
||||
|
||||
if ( is_wp_error( $result ) && $result->has_errors() ) {
|
||||
$errors->merge_from( $result );
|
||||
}
|
||||
|
||||
return $errors->has_errors( $errors ) ? $errors : true;
|
||||
}
|
||||
|
||||
|
|
|
@ -74,6 +74,7 @@ class CheckoutSchema extends AbstractSchema {
|
|||
* @return array
|
||||
*/
|
||||
public function get_properties() {
|
||||
$additional_field_schema = $this->get_additional_fields_schema();
|
||||
return [
|
||||
'order_id' => [
|
||||
'description' => __( 'The order ID to process during checkout.', 'woocommerce' ),
|
||||
|
@ -182,12 +183,12 @@ class CheckoutSchema extends AbstractSchema {
|
|||
'description' => __( 'Additional fields to be persisted on the order.', 'woocommerce' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'properties' => $this->get_additional_fields_schema(),
|
||||
'properties' => $additional_field_schema,
|
||||
'arg_options' => [
|
||||
'sanitize_callback' => [ $this, 'sanitize_additional_fields' ],
|
||||
'validate_callback' => [ $this, 'validate_additional_fields' ],
|
||||
],
|
||||
'required' => $this->is_additional_fields_required(),
|
||||
'required' => $this->schema_has_required_property( $additional_field_schema ),
|
||||
],
|
||||
self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ),
|
||||
];
|
||||
|
@ -282,10 +283,22 @@ class CheckoutSchema extends AbstractSchema {
|
|||
* @return array
|
||||
*/
|
||||
protected function get_additional_fields_schema() {
|
||||
$order_only_fields = $this->additional_fields_controller->get_order_only_fields();
|
||||
return $this->generate_additional_fields_schema(
|
||||
$this->additional_fields_controller->get_fields_for_location( 'contact' ),
|
||||
$this->additional_fields_controller->get_fields_for_location( 'additional' )
|
||||
);
|
||||
}
|
||||
|
||||
$schema = [];
|
||||
foreach ( $order_only_fields as $key => $field ) {
|
||||
/**
|
||||
* Generate the schema for additional fields.
|
||||
*
|
||||
* @param array[] ...$args One or more arrays of additional fields.
|
||||
* @return array
|
||||
*/
|
||||
protected function generate_additional_fields_schema( ...$args ) {
|
||||
$additional_fields = array_merge( ...$args );
|
||||
$schema = [];
|
||||
foreach ( $additional_fields as $key => $field ) {
|
||||
$field_schema = [
|
||||
'description' => $field['label'],
|
||||
'type' => 'string',
|
||||
|
@ -314,10 +327,10 @@ class CheckoutSchema extends AbstractSchema {
|
|||
/**
|
||||
* Check if any additional field is required, so that the parent item is required as well.
|
||||
*
|
||||
* @param array $additional_fields_schema Additional fields schema.
|
||||
* @return bool
|
||||
*/
|
||||
public function is_additional_fields_required() {
|
||||
$additional_fields_schema = $this->get_additional_fields_schema();
|
||||
protected function schema_has_required_property( $additional_fields_schema ) {
|
||||
return array_reduce(
|
||||
array_keys( $additional_fields_schema ),
|
||||
function( $carry, $key ) use ( $additional_fields_schema ) {
|
||||
|
@ -336,19 +349,21 @@ class CheckoutSchema extends AbstractSchema {
|
|||
public function sanitize_additional_fields( $fields ) {
|
||||
$properties = $this->get_additional_fields_schema();
|
||||
$sanitization_utils = new SanitizationUtils();
|
||||
|
||||
$fields = array_reduce(
|
||||
array_keys( $fields ),
|
||||
function( $carry, $key ) use ( $fields, $properties ) {
|
||||
if ( ! isset( $properties[ $key ] ) ) {
|
||||
$fields = $sanitization_utils->wp_kses_array(
|
||||
array_reduce(
|
||||
array_keys( $fields ),
|
||||
function( $carry, $key ) use ( $fields, $properties ) {
|
||||
if ( ! isset( $properties[ $key ] ) ) {
|
||||
return $carry;
|
||||
}
|
||||
$field_schema = $properties[ $key ];
|
||||
$rest_sanitized = rest_sanitize_value_from_schema( wp_unslash( $fields[ $key ] ), $field_schema, $key );
|
||||
$rest_sanitized = $this->additional_fields_controller->sanitize_field( $key, $rest_sanitized );
|
||||
$carry[ $key ] = $rest_sanitized;
|
||||
return $carry;
|
||||
}
|
||||
$field_schema = $properties[ $key ];
|
||||
$rest_sanitized = rest_sanitize_value_from_schema( wp_unslash( $fields[ $key ] ), $field_schema, $key );
|
||||
$carry[ $key ] = $rest_sanitized;
|
||||
return $carry;
|
||||
},
|
||||
[]
|
||||
},
|
||||
[]
|
||||
)
|
||||
);
|
||||
|
||||
return $sanitization_utils->wp_kses_array( $fields );
|
||||
|
@ -364,32 +379,30 @@ class CheckoutSchema extends AbstractSchema {
|
|||
* @return true|\WP_Error
|
||||
*/
|
||||
public function validate_additional_fields( $fields, $request ) {
|
||||
$errors = new \WP_Error();
|
||||
$fields = $this->sanitize_additional_fields( $fields, $request );
|
||||
$properties = $this->get_additional_fields_schema();
|
||||
$errors = new \WP_Error();
|
||||
$fields = $this->sanitize_additional_fields( $fields );
|
||||
$additional_field_schema = $this->get_additional_fields_schema();
|
||||
|
||||
foreach ( array_keys( $properties ) as $key ) {
|
||||
if ( ! isset( $fields[ $key ] ) && false === $properties[ $key ]['required'] ) {
|
||||
// Validate individual properties first.
|
||||
foreach ( $fields as $key => $field_value ) {
|
||||
if ( ! isset( $additional_field_schema[ $key ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$field_value = isset( $fields[ $key ] ) ? $fields[ $key ] : null;
|
||||
|
||||
$result = rest_validate_value_from_schema( $field_value, $properties[ $key ], $key );
|
||||
$result = rest_validate_value_from_schema( $field_value, $additional_field_schema[ $key ], $key );
|
||||
|
||||
// Only allow custom validation on fields that pass the schema validation.
|
||||
if ( true === $result ) {
|
||||
$result = $this->additional_fields_controller->validate_field( $key, $field_value, $request );
|
||||
$result = $this->additional_fields_controller->validate_field( $key, $field_value );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $result ) && $result->has_errors() ) {
|
||||
$location = $this->additional_fields_controller->get_field_location( $key );
|
||||
foreach ( $result->get_error_codes() as $code ) {
|
||||
$result->add_data(
|
||||
[
|
||||
array(
|
||||
'location' => $location,
|
||||
'key' => $key,
|
||||
],
|
||||
),
|
||||
$code
|
||||
);
|
||||
}
|
||||
|
@ -397,6 +410,18 @@ class CheckoutSchema extends AbstractSchema {
|
|||
}
|
||||
}
|
||||
|
||||
// Validate groups of properties per registered location.
|
||||
$locations = array( 'contact', 'additional' );
|
||||
|
||||
foreach ( $locations as $location ) {
|
||||
$location_fields = $this->additional_fields_controller->filter_fields_for_location( $fields, $location );
|
||||
$result = $this->additional_fields_controller->validate_fields_for_location( $location_fields, $location );
|
||||
|
||||
if ( is_wp_error( $result ) && $result->has_errors() ) {
|
||||
$errors->merge_from( $result );
|
||||
}
|
||||
}
|
||||
|
||||
return $errors->has_errors( $errors ) ? $errors : true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -339,6 +339,7 @@ class OrderController {
|
|||
|
||||
$errors_by_code = array();
|
||||
$error_codes = $errors->get_error_codes();
|
||||
|
||||
foreach ( $error_codes as $code ) {
|
||||
$errors_by_code[ $code ] = $errors->get_error_messages( $code );
|
||||
}
|
||||
|
@ -417,6 +418,16 @@ class OrderController {
|
|||
$errors->add( $address_type, sprintf( __( '%s is required', 'woocommerce' ), $address_field['label'] ), $address_field_key );
|
||||
}
|
||||
}
|
||||
|
||||
// Validate additional fields.
|
||||
$result = $this->additional_fields_controller->validate_fields_for_location( $address, 'address', $address_type );
|
||||
|
||||
if ( $result->has_errors() ) {
|
||||
// Add errors to main error object but ensure they maintain the billing/shipping error code.
|
||||
foreach ( $result->get_error_codes() as $code ) {
|
||||
$errors->add( $address_type, $result->get_error_message( $code ), $code );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue