validation_util, $schema ) { switch ( $key ) { case 'country': $carry[ $key ] = wc_strtoupper( sanitize_text_field( wp_unslash( $address[ $key ] ) ) ); break; case 'state': $carry[ $key ] = $validation_util->format_state( sanitize_text_field( wp_unslash( $address[ $key ] ) ), $address['country'] ); break; case 'postcode': $carry[ $key ] = $address['postcode'] ? wc_format_postcode( sanitize_text_field( wp_unslash( $address['postcode'] ) ), $address['country'] ) : ''; break; default: $carry[ $key ] = rest_sanitize_value_from_schema( wp_unslash( $address[ $key ] ), $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; }, [] ); return $sanitization_util->wp_kses_array( $address ); } /** * Validate the given address object. * * @see rest_validate_value_from_schema * * @param array $address Value being sanitized. * @param \WP_REST_Request $request The Request. * @param string $param The param being sanitized. * @return true|\WP_Error */ public function validate_callback( $address, $request, $param ) { $errors = new \WP_Error(); $address = (array) $address; $validation_util = new ValidationUtils(); $schema = $this->get_properties(); // Omit all keys from address that are not in the schema. This should account for email. $address = array_intersect_key( $address, $schema ); // The flow is Validate -> Sanitize -> Re-Validate // First validation step is to ensure fields match their schema, then we sanitize to put them in the // correct format, and finally the second validation step is to ensure the correctly-formatted values // match what we expect (postcode etc.). foreach ( $address as $key => $value ) { // Only run specific validation on properties that are defined in the schema and present in the address. // This is for partial address pushes when only part of a customer address is sent. // Full schema address validation still happens later, so empty, required values are disallowed. if ( empty( $schema[ $key ] ) || empty( $address[ $key ] ) ) { continue; } if ( is_wp_error( rest_validate_value_from_schema( $value, $schema[ $key ], $key ) ) ) { $errors->add( 'invalid_' . $key, sprintf( /* translators: %s: field name */ __( 'Invalid %s provided.', 'woocommerce' ), $key ) ); } } // This condition will be true if any validation errors were encountered, e.g. wrong type supplied or invalid // option in enum fields. if ( $errors->has_errors() ) { return $errors; } $address = $this->sanitize_callback( $address, $request, $param ); if ( ! empty( $address['country'] ) && ! in_array( $address['country'], array_keys( wc()->countries->get_countries() ), true ) ) { $errors->add( 'invalid_country', sprintf( /* translators: %s valid country codes */ __( 'Invalid country code provided. Must be one of: %s', 'woocommerce' ), implode( ', ', array_keys( wc()->countries->get_countries() ) ) ) ); return $errors; } if ( ! empty( $address['state'] ) && ! $validation_util->validate_state( $address['state'], $address['country'] ) ) { $errors->add( 'invalid_state', sprintf( /* translators: %1$s given state, %2$s valid states */ __( 'The provided state (%1$s) is not valid. Must be one of: %2$s', 'woocommerce' ), esc_html( $address['state'] ), implode( ', ', array_keys( $validation_util->get_states_for_country( $address['country'] ) ) ) ) ); } if ( ! empty( $address['postcode'] ) && ! \WC_Validation::is_postcode( $address['postcode'], $address['country'] ) ) { $errors->add( 'invalid_postcode', __( 'The provided postcode / ZIP is not valid', 'woocommerce' ) ); } if ( ! empty( $address['phone'] ) && ! \WC_Validation::is_phone( $address['phone'] ) ) { $errors->add( 'invalid_phone', __( 'The provided phone number is not valid', 'woocommerce' ) ); } // Get additional field keys here as we need to know if they are present in the address for validation. $additional_keys = array_keys( $this->get_additional_address_fields_schema() ); foreach ( array_keys( $address ) as $key ) { // Skip email here it will be validated in BillingAddressSchema. if ( 'email' === $key ) { continue; } // Only run specific validation on properties that are defined in the schema and present in the address. // This is for partial address pushes when only part of a customer address is sent. // Full schema address validation still happens later, so empty, required values are disallowed. if ( empty( $schema[ $key ] ) || empty( $address[ $key ] ) ) { continue; } $result = rest_validate_value_from_schema( $address[ $key ], $schema[ $key ], $key ); // 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 ) ) { $result = $this->additional_fields_controller->validate_field( $key, $address[ $key ] ); } if ( is_wp_error( $result ) && $result->has_errors() ) { $errors->merge_from( $result ); } } $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; } /** * Get additional address fields schema. * * @return array */ protected function get_additional_address_fields_schema() { $additional_fields_keys = $this->additional_fields_controller->get_address_fields_keys(); $fields = $this->additional_fields_controller->get_additional_fields(); $address_fields = array_filter( $fields, function ( $key ) use ( $additional_fields_keys ) { return in_array( $key, $additional_fields_keys, true ); }, ARRAY_FILTER_USE_KEY ); $schema = []; foreach ( $address_fields as $key => $field ) { $field_schema = [ 'description' => $field['label'], 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => $field['required'], ]; if ( 'select' === $field['type'] ) { $field_schema['enum'] = array_map( function ( $option ) { return $option['value']; }, $field['options'] ); } if ( 'checkbox' === $field['type'] ) { $field_schema['type'] = 'boolean'; } $schema[ $key ] = $field_schema; } return $schema; } } validation_util, $schema ) { switch ( $key ) { case 'country': $carry[ $key ] = wc_strtoupper( sanitize_text_field( wp_unslash( $address[ $key ] ) ) ); break; case 'state': $carry[ $key ] = $validation_util->format_state( sanitize_text_field( wp_unslash( $address[ $key ] ) ), $address['country'] ); break; case 'postcode': $carry[ $key ] = $address['postcode'] ? wc_format_postcode( sanitize_text_field( wp_unslash( $address['postcode'] ) ), $address['country'] ) : ''; break; default: $carry[ $key ] = rest_sanitize_value_from_schema( wp_unslash( $address[ $key ] ), $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; }, [] ); return $sanitization_util->wp_kses_array( $address ); } /** * Validate the given address object. * * @see rest_validate_value_from_schema * * @param array $address Value being sanitized. * @param \WP_REST_Request $request The Request. * @param string $param The param being sanitized. * @return true|\WP_Error */ public function validate_callback( $address, $request, $param ) { $errors = new \WP_Error(); $address = (array) $address; $validation_util = new ValidationUtils(); $schema = $this->get_properties(); // Omit all keys from address that are not in the schema. This should account for email. $address = array_intersect_key( $address, $schema ); // The flow is Validate -> Sanitize -> Re-Validate // First validation step is to ensure fields match their schema, then we sanitize to put them in the // correct format, and finally the second validation step is to ensure the correctly-formatted values // match what we expect (postcode etc.). foreach ( $address as $key => $value ) { // Only run specific validation on properties that are defined in the schema and present in the address. // This is for partial address pushes when only part of a customer address is sent. // Full schema address validation still happens later, so empty, required values are disallowed. if ( empty( $schema[ $key ] ) || empty( $address[ $key ] ) ) { continue; } if ( is_wp_error( rest_validate_value_from_schema( $value, $schema[ $key ], $key ) ) ) { $errors->add( 'invalid_' . $key, sprintf( /* translators: %s: field name */ __( 'Invalid %s provided.', 'woocommerce' ), $key ) ); } } // This condition will be true if any validation errors were encountered, e.g. wrong type supplied or invalid // option in enum fields. if ( $errors->has_errors() ) { return $errors; } $address = $this->sanitize_callback( $address, $request, $param ); if ( ! empty( $address['country'] ) && ! in_array( $address['country'], array_keys( wc()->countries->get_countries() ), true ) ) { $errors->add( 'invalid_country', sprintf( /* translators: %s valid country codes */ __( 'Invalid country code provided. Must be one of: %s', 'woocommerce' ), implode( ', ', array_keys( wc()->countries->get_countries() ) ) ) ); return $errors; } if ( ! empty( $address['state'] ) && ! $validation_util->validate_state( $address['state'], $address['country'] ) ) { $errors->add( 'invalid_state', sprintf( /* translators: %1$s given state, %2$s valid states */ __( 'The provided state (%1$s) is not valid. Must be one of: %2$s', 'woocommerce' ), esc_html( $address['state'] ), implode( ', ', array_keys( $validation_util->get_states_for_country( $address['country'] ) ) ) ) ); } if ( ! empty( $address['postcode'] ) && ! \WC_Validation::is_postcode( $address['postcode'], $address['country'] ) ) { $errors->add( 'invalid_postcode', __( 'The provided postcode / ZIP is not valid', 'woocommerce' ) ); } if ( ! empty( $address['phone'] ) && ! \WC_Validation::is_phone( $address['phone'] ) ) { $errors->add( 'invalid_phone', __( 'The provided phone number is not valid', 'woocommerce' ) ); } // Get additional field keys here as we need to know if they are present in the address for validation. $additional_keys = array_keys( $this->get_additional_address_fields_schema() ); foreach ( array_keys( $address ) as $key ) { // Skip email here it will be validated in BillingAddressSchema. if ( 'email' === $key ) { continue; } // Only run specific validation on properties that are defined in the schema and present in the address. // This is for partial address pushes when only part of a customer address is sent. // Full schema address validation still happens later, so empty, required values are disallowed. if ( empty( $schema[ $key ] ) || empty( $address[ $key ] ) ) { continue; } $result = rest_validate_value_from_schema( $address[ $key ], $schema[ $key ], $key ); // 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 ) ) { $result = $this->additional_fields_controller->validate_field( $key, $address[ $key ] ); } if ( is_wp_error( $result ) && $result->has_errors() ) { $errors->merge_from( $result ); } } $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; } /** * Get additional address fields schema. * * @return array */ protected function get_additional_address_fields_schema() { $additional_fields_keys = $this->additional_fields_controller->get_address_fields_keys(); $fields = $this->additional_fields_controller->get_additional_fields(); $address_fields = array_filter( $fields, function ( $key ) use ( $additional_fields_keys ) { return in_array( $key, $additional_fields_keys, true ); }, ARRAY_FILTER_USE_KEY ); $schema = []; foreach ( $address_fields as $key => $field ) { $field_schema = [ 'description' => $field['label'], 'type' => 'string', 'context' => [ 'view', 'edit' ], 'required' => $field['required'], ]; if ( 'select' === $field['type'] ) { $field_schema['enum'] = array_map( function ( $option ) { return $option['value']; }, $field['options'] ); } if ( 'checkbox' === $field['type'] ) { $field_schema['type'] = 'boolean'; } $schema[ $key ] = $field_schema; } return $schema; } }