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_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( 'Field validation for %s encountered an error. %s', esc_html( $field_key ), esc_html( $e->getMessage() ) ), E_USER_WARNING ); } return $errors; } /** * Update the default locale with additional fields without country limitations. * * @param array $locale The locale to update. * @return mixed */ public function update_default_locale_with_fields( $locale ) { foreach ( $this->fields_locations['address'] as $field_id => $additional_field ) { if ( empty( $locale[ $field_id ] ) ) { $locale[ $field_id ] = $additional_field; } } return $locale; } /** * Returns an array of fields keys for the address location. * * @return array An array of fields keys. */ public function get_address_fields_keys() { return $this->fields_locations['address']; } /** * Returns an array of fields keys for the contact location. * * @return array An array of fields keys. */ public function get_contact_fields_keys() { return $this->fields_locations['contact']; } /** * Returns an array of fields keys for the additional area location. * * @return array An array of fields keys. * @deprecated 8.9.0 Use get_order_fields_keys instead. */ public function get_additional_fields_keys() { wc_deprecated_function( __METHOD__, '8.9.0', 'get_order_fields_keys' ); return $this->get_order_fields_keys(); } /** * Returns an array of fields keys for the additional area group. * * @return array An array of fields keys. */ public function get_order_fields_keys() { return $this->fields_locations['order']; } /** * Returns an array of fields for a given location. * * @param string $location The location to get fields for (address|contact|order). * @return array An array of fields definitions. */ public function get_fields_for_location( $location ) { if ( 'additional' === $location ) { wc_deprecated_argument( 'location', '8.9.0', 'The "additional" location is deprecated. Use "order" instead.' ); $location = 'order'; } if ( in_array( $location, array_keys( $this->fields_locations ), true ) ) { $order_fields_keys = $this->fields_locations[ $location ]; 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 ); } return []; } /** * 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|order). * @param string $group The group to get the field value for (shipping|billing|other). * @return WP_Error */ public function validate_fields_for_location( $fields, $location, $group = 'other' ) { $errors = new WP_Error(); if ( 'additional' === $location ) { wc_deprecated_argument( 'location', '8.9.0', 'The "additional" location is deprecated. Use "order" instead.' ); $location = 'order'; } if ( 'additional' === $group ) { wc_deprecated_argument( 'group', '8.9.0', 'The "additional" group is deprecated. Use "other" instead.' ); $group = 'other'; } 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|other). * * @since 8.6.0 * @deprecated 8.9.0 Use woocommerce_blocks_validate_location_order_fields instead. */ wc_do_deprecated_action( 'woocommerce_blocks_validate_location_additional_fields', array( $errors, $fields, $group ), '8.9.0', 'woocommerce_blocks_validate_location_additional_fields', 'This action has been graduated, use woocommerce_blocks_validate_location_additional_fields instead.' ); /** * 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|other). * * @since 8.6.0 * @deprecated 8.9.0 Use woocommerce_blocks_validate_location_{location}_fields instead. */ wc_do_deprecated_action( '__experimental_woocommerce_blocks_validate_location_' . $location . '_fields', array( $errors, $fields, $group ), '8.9.0', 'woocommerce_blocks_validate_location_' . $location . '_fields', 'This action has been graduated, use woocommerce_blocks_validate_location_' . $location . '_fields instead.' ); /** * 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|other). * * @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|order). * * @return true|WP_Error True if the field is valid, a WP_Error otherwise. */ public function validate_field_for_location( $key, $value, $location ) { if ( 'additional' === $location ) { wc_deprecated_argument( 'location', '8.9.0', 'The "additional" location is deprecated. Use "order" instead.' ); $location = 'order'; } if ( ! $this->is_field( $key ) ) { return new WP_Error( 'woocommerce_invalid_checkout_field', \sprintf( // translators: % is field key. __( 'The field %s is invalid.', 'woocommerce' ), $key ) ); } if ( ! in_array( $key, $this->fields_locations[ $location ], true ) ) { return new WP_Error( 'woocommerce_invalid_checkout_field_location', \sprintf( // translators: %1$s is field key, %2$s location. __( 'The field %1$s is invalid for the location %2$s.', 'woocommerce' ), $key, $location ) ); } $field = $this->additional_fields[ $key ]; if ( ! empty( $field['required'] ) && empty( $value ) ) { return new WP_Error( 'woocommerce_required_checkout_field', \sprintf( // translators: %s is field key. __( 'The field %s is required.', 'woocommerce' ), $key ) ); } return true; } /** * Returns all fields key for a given group. * * @param string $group The group to get the key for (shipping|billing|other). * * @return string[] Field keys. */ public function get_fields_for_group( $group = 'other' ) { if ( 'shipping' === $group ) { return $this->get_fields_for_location( 'address' ); } if ( 'billing' === $group ) { return $this->get_fields_for_location( 'address' ); } return \array_merge( $this->get_fields_for_location( 'contact' ), $this->get_fields_for_location( 'order' ) ); } /** * Returns true if the given key is a valid field. * * @param string $key The field key. * * @return bool True if the field is valid, false otherwise. */ public function is_field( $key ) { return array_key_exists( $key, $this->additional_fields ); } /** * Returns true if the given key is a valid customer field. * * Customer fields are fields saved to the customer data, like address and contact fields. * * @param string $key The field key. * * @return bool True if the field is valid, false otherwise. */ public function is_customer_field( $key ) { return in_array( $key, array_intersect( array_merge( $this->get_address_fields_keys(), $this->get_contact_fields_keys() ), array_keys( $this->additional_fields ) ), true ); } /** * Persists a field value for a given order. This would also optionally set the field value on the customer object if the order is linked to a registered customer. * * @param string $key The field key. * @param mixed $value The field value. * @param WC_Order $order The order to persist the field for. * @param string $group The group to persist the field for (shipping|billing|other). * @param bool $set_customer Whether to set the field value on the customer or not. * * @return void */ public function persist_field_for_order( string $key, $value, WC_Order $order, string $group = 'other', bool $set_customer = true ) { if ( 'additional' === $group ) { wc_deprecated_argument( 'group', '8.9.0', 'The "additional" group is deprecated. Use "other" instead.' ); $group = 'other'; } $this->set_array_meta( $key, $value, $order, $group ); if ( $set_customer && $order->get_customer_id() ) { $customer = new WC_Customer( $order->get_customer_id() ); $this->persist_field_for_customer( $key, $value, $customer, $group ); } } /** * Persists a field value for a given customer. * * @param string $key The field key. * @param mixed $value The field value. * @param WC_Customer $customer The customer to persist the field for. * @param string $group The group to persist the field for (shipping|billing|other). * * @return void */ public function persist_field_for_customer( string $key, $value, WC_Customer $customer, string $group = 'other' ) { if ( 'additional' === $group ) { wc_deprecated_argument( 'group', '8.9.0', 'The "additional" group is deprecated. Use "other" instead.' ); $group = 'other'; } $this->set_array_meta( $key, $value, $customer, $group ); } /** * Sets a field value in an array meta, supporting routing things to billing, shipping, or additional fields, based on a prefix for the key. * * @param string $key The field key. * @param mixed $value The field value. * @param WC_Customer|WC_Order $wc_object The object to set the field value for. * @param string $group The group to set the field value for (shipping|billing|other). * * @return void */ private function set_array_meta( string $key, $value, WC_Data $wc_object, string $group ) { $meta_key = self::get_group_key( $group ) . $key; /** * Allow reacting for saving an additional field value. * * @param string $key The key of the field being saved. * @param mixed $value The value of the field being saved. * @param string $group The group of this location (shipping|billing|other). * @param WC_Customer|WC_Order $wc_object The object to set the field value for. * * @since 8.9.0 */ do_action( 'woocommerce_set_additional_field_value', $key, $value, $group, $wc_object ); // Convert boolean values to strings because Data Stores will skip false values. if ( is_bool( $value ) ) { $value = $value ? '1' : '0'; } // Replacing all meta using `add_meta_data`. For some reason `update_meta_data` causes duplicate keys. $wc_object->add_meta_data( $meta_key, $value, true ); } /** * Returns a field value for a given object. * * @param string $key The field key. * @param WC_Customer|WC_Order $wc_object The customer or order to get the field value for. * @param string $group The group to get the field value for (shipping|billing|other). * * @return mixed The field value. */ public function get_field_from_object( string $key, WC_Data $wc_object, string $group = 'other' ) { if ( 'additional' === $group ) { wc_deprecated_argument( 'group', '8.9.0', 'The "additional" group is deprecated. Use "other" instead.' ); $group = 'other'; } $meta_key = self::get_group_key( $group ) . $key; $value = $wc_object->get_meta( $meta_key, true ); if ( ! $value ) { /** * Allow providing a default value for additional fields if no value is already set. * * @param null $value The default value for the filter, always null. * @param string $group The group of this key (shipping|billing|other). * @param WC_Data $wc_object The object to get the field value for. * * @since 8.9.0 */ $value = apply_filters( "woocommerce_get_default_value_for_{$key}", null, $group, $wc_object ); } // We cast the value to a boolean if the field is a checkbox. if ( $this->is_field( $key ) && 'checkbox' === $this->additional_fields[ $key ]['type'] ) { return '1' === $value; } if ( null === $value ) { return ''; } return $value; } /** * Returns an array of all fields values for a given object in a group. * * @param WC_Data $wc_object The object or order to get the fields for. * @param string $group The group to get the fields for (shipping|billing|other). * @param bool $all Whether to return all fields or only the ones that are still registered. Default false. * * @return array An array of fields. */ public function get_all_fields_from_object( WC_Data $wc_object, string $group = 'other', bool $all = false ) { if ( 'additional' === $group ) { wc_deprecated_argument( 'group', '8.9.0', 'The "additional" group is deprecated. Use "other" instead.' ); $group = 'other'; } $meta_data = []; $prefix = self::get_group_key( $group ); if ( $wc_object instanceof WC_Data ) { $meta = $wc_object->get_meta_data(); foreach ( $meta as $meta_data_object ) { if ( 0 === \strpos( $meta_data_object->key, $prefix ) ) { $key = \str_replace( $prefix, '', $meta_data_object->key ); if ( $all || $this->is_field( $key ) ) { $meta_data[ $key ] = $meta_data_object->value; } } } } $missing_fields = array_diff( array_keys( $this->get_fields_for_group( $group ) ), array_keys( $meta_data ) ); foreach ( $missing_fields as $missing_field ) { /** * Allow providing a default value for additional fields if no value is already set. * * @param null $value The default value for the filter, always null. * @param string $group The group of this key (shipping|billing|other). * @param WC_Data $wc_object The object to get the field value for. * * @since 8.9.0 */ $value = apply_filters( "woocommerce_get_default_value_for_{$missing_field}", null, $group, $wc_object ); if ( $value ) { $meta_data[ $missing_field ] = $value; } } return $meta_data; } /** * Copies additional fields from an order to a customer. * * @param WC_Order $order The order to sync the fields for. * @param WC_Customer $customer The customer to sync the fields for. */ public function sync_customer_additional_fields_with_order( WC_Order $order, WC_Customer $customer ) { foreach ( $this->groups as $group ) { $order_additional_fields = $this->get_all_fields_from_object( $order, $group, true ); // Sync customer additional fields with order additional fields. foreach ( $order_additional_fields as $key => $value ) { if ( $this->is_customer_field( $key ) ) { $this->persist_field_for_customer( $key, $value, $customer, $group ); } } } } /** * Copies additional fields from a customer to an order. * * @param WC_Order $order The order to sync the fields for. * @param WC_Customer $customer The customer to sync the fields for. */ public function sync_order_additional_fields_with_customer( WC_Order $order, WC_Customer $customer ) { foreach ( $this->groups as $group ) { $customer_additional_fields = $this->get_all_fields_from_object( $customer, $group, true ); // Sync order additional fields with customer additional fields. foreach ( $customer_additional_fields as $key => $value ) { if ( $this->is_field( $key ) ) { $this->persist_field_for_order( $key, $value, $order, $group, false ); } } } } /** * 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|order). * @return array The filtered fields. */ public function filter_fields_for_location( array $fields, string $location ) { if ( 'additional' === $location ) { wc_deprecated_argument( 'location', '8.9.0', 'The "additional" location is deprecated. Use "order" instead.' ); $location = 'order'; } return array_filter( $fields, function ( $key ) use ( $location ) { return $this->is_field( $key ) && $this->get_field_location( $key ) === $location; }, ARRAY_FILTER_USE_KEY ); } /** * Filter fields for order confirmation. * * @param array $fields The fields to filter. * @return array The filtered fields. */ public function filter_fields_for_order_confirmation( $fields ) { return array_filter( $fields, function ( $field ) { return ! empty( $field['show_in_order_confirmation'] ); } ); } /** * Get additional fields for an order. * * @param WC_Order $order Order object. * @param string $location The location to get fields for (address|contact|order). * @param string $group The group to get the field value for (shipping|billing|other). * @param string $context The context to get the field value for (edit|view). * @return array An array of fields definitions as well as their values formatted for display. */ public function get_order_additional_fields_with_values( WC_Order $order, string $location, string $group = 'other', string $context = 'edit' ) { // Because the Additional Checkout Fields API only applies to orders created with Store API, we should not // return any values unless it was created using Store API. This is mainly to prevent "empty" checkbox values // from being shown on the order confirmation page for orders placed using the shortcode. It's rare that this // will happen but not impossible. if ( 'store-api' !== $order->get_created_via() ) { return []; } if ( 'additional' === $location ) { wc_deprecated_argument( 'location', '8.9.0', 'The "additional" location is deprecated. Use "order" instead.' ); $location = 'order'; } if ( 'additional' === $group ) { wc_deprecated_argument( 'group', '8.9.0', 'The "additional" group is deprecated. Use "other" instead.' ); $group = 'other'; } $fields = $this->get_fields_for_location( $location ); $fields_with_values = []; foreach ( $fields as $field_key => $field ) { $value = $this->get_field_from_object( $field_key, $order, $group ); if ( '' === $value || null === $value ) { continue; } if ( 'view' === $context ) { $value = $this->format_additional_field_value( $value, $field ); } $field['value'] = $value; $fields_with_values[ $field_key ] = $field; } return $fields_with_values; } /** * Formats a raw field value for display based on its type definition. * * @param string $value Value to format. * @param array $field Additional field definition. * @return string */ public function format_additional_field_value( $value, $field ) { if ( 'checkbox' === $field['type'] ) { $value = $value ? __( 'Yes', 'woocommerce' ) : __( 'No', 'woocommerce' ); } if ( 'select' === $field['type'] ) { $options = array_column( $field['options'], 'label', 'value' ); $value = isset( $options[ $value ] ) ? $options[ $value ] : $value; } return $value; } /** * Returns a group meta prefix based on its name. * * @param string $group_name The group name (billing|shipping|other). * @return string The group meta prefix. */ public static function get_group_key( $group_name ) { if ( 'additional' === $group_name ) { wc_deprecated_argument( 'group_name', '8.9.0', 'The "additional" group is deprecated. Use "other" instead.' ); $group_name = 'other'; } if ( 'billing' === $group_name ) { return self::BILLING_FIELDS_PREFIX; } if ( 'shipping' === $group_name ) { return self::SHIPPING_FIELDS_PREFIX; } return self::OTHER_FIELDS_PREFIX; } /** * Returns a group name based on passed group key. * * @param string $group_key The group name (_wc_billing|_wc_shipping|_wc_other). * @return string The group meta prefix. */ public static function get_group_name( $group_key ) { if ( '_wc_additional' === $group_key ) { wc_deprecated_argument( 'group_key', '8.9.0', 'The "_wc_additional" group key is deprecated. Use "_wc_other" instead.' ); $group_key = '_wc_other'; } if ( 0 === \strpos( self::BILLING_FIELDS_PREFIX, $group_key ) ) { return 'billing'; } if ( 0 === \strpos( self::SHIPPING_FIELDS_PREFIX, $group_key ) ) { return 'shipping'; } return 'other'; } }