Your IP : 216.73.217.92


Current Path : /home/blindman/www/wp-content/plugins/gravityforms/includes/fields/
Upload File :
Current File : /home/blindman/www/wp-content/plugins/gravityforms/includes/fields/class-gf-field-phone.php

<?php

// If Gravity Forms isn't loaded, bail.
if ( ! class_exists( 'GFForms' ) ) {
	die();
}

/**
 * Class GF_Field_Phone
 *
 * Handles the behavior of Phone fields.
 *
 * @since Unknown
 */
class GF_Field_Phone extends GF_Field {

	/**
	 * Defines the field type.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @var string The field type.
	 */
	public $type = 'phone';

	/**
	 * Whether this field allows links/URLs in the value.
	 *
	 * @since 3.1.0
	 *
	 * @var bool
	 */
	public $noURLs = true;

	/**
	 * Defines the field title to be used in the form editor.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @used-by GFCommon::get_field_type_title()
	 *
	 * @return string The field title. Translatable and escaped.
	 */
	public function get_form_editor_field_title() {
		return esc_attr__( 'Phone', 'gravityforms' );
	}

	/**
	 * Returns the field's form editor description.
	 *
	 * @since 2.5
	 *
	 * @return string
	 */
	public function get_form_editor_field_description() {
		return esc_attr__( 'Allows users to enter a phone number.', 'gravityforms' );
	}

	/**
	 * Returns the field's form editor icon.
	 *
	 * This could be an icon url or a gform-icon class.
	 *
	 * @since 2.5
	 *
	 * @return string
	 */
	public function get_form_editor_field_icon() {
		return 'gform-icon--phone';
	}

	/**
	 * Defines the field settings available within the field editor.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @return array The field settings available for the field.
	 */
	function get_form_editor_field_settings() {
		return array(
			'conditional_logic_field_setting',
			'prepopulate_field_setting',
			'error_message_setting',
			'label_setting',
			'label_placement_setting',
			'sub_label_placement_setting',
			'phone_sub_labels_setting',
			'admin_label_setting',
			'size_setting',
			'rules_setting',
			'visibility_setting',
			'duplicate_setting',
			'default_value_setting',
			'placeholder_setting',
			'description_setting',
			'phone_format_setting',
			'default_country_setting',
			'show_country_code_setting',
			'css_class_setting',
			'autocomplete_setting',
		);
	}

	/**
	 * Defines if conditional logic is supported in this field type.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @used-by GFFormDetail::inline_scripts()
	 * @used-by GFFormSettings::output_field_scripts()
	 *
	 * @return bool true
	 */
	public function is_conditional_logic_supported() {
		return true;
	}

	/**
	 * Returns the placeholder attribute for the field.
	 *
	 * If no custom placeholder is set and the phone format has a mask, uses the mask as placeholder.
	 *
	 * @since 3.0.0
	 *
	 * @return string
	 */
	public function get_field_placeholder_attribute() {
		if ( empty( $this->placeholder ) ) {
			$phone_format = $this->get_phone_format();
			if ( rgar( $phone_format, 'mask' ) ) {
				return sprintf( "placeholder='%s'", esc_attr( $phone_format['mask'] ) );
			}
		}

		return parent::get_field_placeholder_attribute();
	}

	/**
	 * Returns the HTML tag for the field container.
	 *
	 * @since 3.0.0
	 *
	 * @param array $form The current Form object.
	 *
	 * @return string
	 */
	public function get_field_container_tag( $form ) {

		if ( GFCommon::is_legacy_markup_enabled( $form ) ) {
			return parent::get_field_container_tag( $form );
		}

		if ( $this->phoneFormat === 'formatted' ) {
			return 'fieldset';
		} else {
			return 'div';
		}

	}

	/**
	 * Validates inputs for the Phone field.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @used-by GFFormDisplay::validate()
	 * @uses    GF_Field_Phone::get_phone_format()
	 * @uses    GF_Field_Phone::$validation_message
	 * @uses    GF_Field_Phone::$errorMessage
	 *
	 * @param array|string $value The field value to be validated.
	 * @param array        $form  The Form Object.
	 *
	 * @return void
	 */
	public function validate( $value, $form ) {
		// Skip validation if value is empty.
		if ( $value === '' || $value === 0 ) {
			return;
		}

		// For formatted international phone, validate JSON structure and E.164 value.
		if ( $this->phoneFormat === 'formatted' ) {
			$decoded = $this->to_array( $value );

			// If it's valid JSON, validate structure
			if ( $decoded ) {
				// Validate required fields exist
				$required_keys = array( 'country', 'national', 'formatted', 'e164' );
				foreach ( $required_keys as $key ) {
					if ( ! isset( $decoded[ $key ] ) ) {
						$this->failed_validation = true;
						$this->validation_message = ! empty( $this->errorMessage ) ? $this->errorMessage : esc_html__( 'Please enter a valid phone number.', 'gravityforms' );
						return;
					}
				}

				// Use E.164 validator on the e164 field
				if ( ! class_exists( 'E164Validator' ) ) {
					require_once GFCommon::get_base_path() . '/includes/validation/class-e164-validator.php';
				}

				$validator         = new E164Validator();
				$validation_result = $validator->validate( $decoded['e164'], true );

				if ( ! $validation_result['valid'] ) {
					$this->failed_validation = true;
					$this->validation_message = ! empty( $this->errorMessage ) ? $this->errorMessage : $validation_result['message'];
				}
				return;
			}

			// If not JSON, treat as invalid
			$this->failed_validation = true;
			$this->validation_message = esc_html__( 'Please enter a valid phone number in the correct format.', 'gravityforms' );
			return;
		}

		// For other formats, use regex validation.
		$phone_format = $this->get_phone_format();
		if ( rgar( $phone_format, 'regex' ) && ! preg_match( $phone_format['regex'], $value ) ) {
			$this->failed_validation  = true;
			$this->validation_message = ! empty( $this->errorMessage )
				? $this->errorMessage
				: sprintf( esc_html__( 'Phone format: %s', 'gravityforms' ), rgar( $phone_format, 'instruction' ) );
		}
	}

	/**
	 * Determines if Links/URLs should be detected.
	 *
	 * @since 3.1.0
	 *
	 * @return bool
	 */
	public function should_detect_urls() {
		return parent::should_detect_urls() && $this->phoneFormat === 'international';
	}

	/**
	 * Get field CSS class.
	 *
	 * @since: next
	 *
	 * @return string
	 */
	public function get_field_css_class() {
		return 'gfield--phone-format-' . $this->phoneFormat;
	}

	/**
	 * Returns the field input.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @used-by GFCommon::get_field_input()
	 * @uses    GF_Field::is_entry_detail()
	 * @uses    GF_Field::is_form_editor()
	 * @uses    GF_Field_Phone::$failed_validation
	 * @uses    GF_Field_Phone::get_phone_format()
	 * @uses    GF_Field::get_field_placeholder_attribute()
	 * @uses    GF_Field_Phone::$isRequired
	 * @uses    GF_Field::get_tabindex()
	 *
	 * @param array      $form  The Form Object.
	 * @param string     $value The value of the input. Defaults to empty string.
	 * @param null|array $entry The Entry Object. Defaults to null.
	 *
	 * @return string The HTML markup for the field.
	 */
	public function get_field_input( $form, $value = '', $entry = null ) {

		if ( is_array( $value ) ) {
			$value = '';
		}
		if ( $this->phoneFormat !== 'formatted' && ! empty( $value ) && GFCommon::is_json( $value ) ) {
            $decoded = $this->to_array( $value );
            if ( $decoded ) {
                $value = ! empty( $decoded['e164'] ) ? $decoded['e164'] : ( ! empty( $decoded['formatted'] ) ? $decoded['formatted'] : ( ! empty( $decoded['national'] ) ? $decoded['national'] : '' ) );
            }
        }

		$is_entry_detail = $this->is_entry_detail();
		$is_form_editor  = $this->is_form_editor();

		$form_id  = $form['id'];
		$id       = intval( $this->id );
		$field_id = $is_entry_detail || $is_form_editor || $form_id == 0 ? "input_$id" : 'input_' . $form_id . "_$id";

		$size          = $this->size;
		$disabled_text = $is_form_editor ? "disabled='disabled'" : '';
		$class_suffix  = $is_entry_detail ? '_admin' : '';
		$class         = $size . $class_suffix;
		$class         = esc_attr( $class );

		$mask_attribute = '';
		$phone_format   = $this->get_phone_format();
		if ( ! empty( $phone_format['mask'] ) ) {
			$mask_attribute = sprintf( 'data-mask="%s"', esc_attr( $phone_format['mask'] ) );
		}

		$html_input_type        = 'tel';
		$placeholder_attribute  = $this->phoneFormat !== 'formatted' ? $this->get_field_placeholder_attribute() : '';
		$required_attribute     = $this->isRequired ? 'aria-required="true"' : '';
		$invalid_attribute      = $this->failed_validation ? 'aria-invalid="true"' : 'aria-invalid="false"';
		$aria_describedby       = $this->get_aria_describedby();
		$autocomplete_attribute = $this->enableAutocomplete ? $this->get_field_autocomplete_attribute() : '';

		$tabindex = $this->get_tabindex();

		// Use pre-rendered HTML for formatted phone fields
		if ( $this->phoneFormat === 'formatted' ) {
			// Generate the pre-rendered international phone HTML
			$phone_html = $this->get_formatted_phone_html(
				$field_id,
				$value,
				$class,
				$tabindex,
				$placeholder_attribute,
				$required_attribute,
				$invalid_attribute,
				$aria_describedby,
				$autocomplete_attribute,
				$disabled_text,
				$form
			);

			// Get default country for data attribute
			$default_country = ! empty( $this->defaultCountry ) ? strtolower( $this->defaultCountry ) : 'us';
			$default_country_attr = sprintf( " data-default-country='%s'", esc_attr( $default_country ) );

			// Get show country code setting (defaults to true)
			$show_country_code = isset( $this->showCountryCode ) ? $this->showCountryCode : true;
			$show_country_code_attr = sprintf( " data-show-country-code='%s'", $show_country_code ? 'true' : 'false' );

			// Wrap in container and add hidden input
			$input = sprintf(
				"<div class='ginput_container ginput_container_phone'>%s<input name='input_%d' id='%s' type='hidden' value='%s' class='%s' data-phone-format='formatted'%s%s %s %s %s/></div>",
				$phone_html,
				$id,
				$field_id,
				esc_attr( $value ),
				esc_attr( $class ),
				$default_country_attr,
				$show_country_code_attr,
				$required_attribute,
				$invalid_attribute,
				$aria_describedby
			);
		} else {
			// For non-formatted phones, use the original simple input
			$input = sprintf( "<div class='ginput_container ginput_container_phone'><input name='input_%d' id='%s' type='{$html_input_type}' value='%s' class='%s' {$tabindex} {$placeholder_attribute} {$required_attribute} {$invalid_attribute} {$aria_describedby} {$autocomplete_attribute} {$mask_attribute} %s/></div>", $id, $field_id, esc_attr( $value ), esc_attr( $class ), $disabled_text );
		}

		return $input;
	}

	/**
	 * Get dial codes from the shared countries.json file.
	 *
	 * @since: next
	 *
	 * @return array Associative array of iso2 => dialCode.
	 */
	private static function get_dial_codes() {
		static $dial_codes = null;

		if ( $dial_codes === null ) {
			$dial_codes = array();
			$json_path  = GFCommon::get_base_path() . '/assets/js/src/theme/fields/international-phone/countries.json';

			if ( file_exists( $json_path ) ) {
				$json_content = file_get_contents( $json_path );
				$countries    = json_decode( $json_content, true );

				if ( is_array( $countries ) ) {
					foreach ( $countries as $country ) {
						if ( isset( $country['iso2'], $country['dialCode'] ) ) {
							$dial_codes[ strtolower( $country['iso2'] ) ] = $country['dialCode'];
						}
					}
				}
			}
		}

		return $dial_codes;
	}

	/**
	 * Get dial code for a country ISO2 code.
	 *
	 * @since: next
	 *
	 * @param string $iso2 The country ISO2 code.
	 *
	 * @return string The dial code or empty string if not found.
	 */
	private static function get_dial_code( $iso2 ) {
		$dial_codes = self::get_dial_codes();
		$iso2       = strtolower( $iso2 );
		return isset( $dial_codes[ $iso2 ] ) ? $dial_codes[ $iso2 ] : '';
	}

	/**
	 * Get country data (dial code and placeholder) for a country ISO2 code.
	 *
	 * @since: next
	 *
	 * @param string $iso2 The country ISO2 code.
	 *
	 * @return array Array with 'dialCode' and 'placeholder' keys.
	 */
	private static function get_country_data( $iso2 ) {
		static $countries = null;

		if ( $countries === null ) {
			$json_path = GFCommon::get_base_path() . '/assets/js/src/theme/fields/international-phone/countries.json';
			$countries = file_exists( $json_path ) ? json_decode( file_get_contents( $json_path ), true ) : array();
		}

		$iso2 = strtolower( $iso2 );
		foreach ( $countries as $country ) {
			if ( isset( $country['iso2'] ) && strtolower( $country['iso2'] ) === $iso2 ) {
				return array(
					'dialCode'    => isset( $country['dialCode'] ) ? $country['dialCode'] : '',
					'placeholder' => isset( $country['placeholder'] ) ? $country['placeholder'] : '',
				);
			}
		}

		return array( 'dialCode' => '', 'placeholder' => '' );
	}

	/**
	 * Generates pre-rendered HTML structure for international phone field
	 *
	 * @since 3.0
	 *
	 * @param string $field_id The field ID
	 * @param string $value The field value
	 * @param string $class The CSS classes
	 * @param string $tabindex The tabindex attribute
	 * @param string $placeholder_attribute The placeholder attribute
	 * @param string $required_attribute The required attribute
	 * @param string $invalid_attribute The invalid attribute
	 * @param string $aria_describedby The aria-describedby attribute
	 * @param string $autocomplete_attribute The autocomplete attribute
	 * @param string $disabled_text The disabled attribute
	 * @param array $form The current form
	 *
	 * @return string The pre-rendered HTML
	 */
	private function get_formatted_phone_html( $field_id, $value, $class, $tabindex, $placeholder_attribute, $required_attribute, $invalid_attribute, $aria_describedby, $autocomplete_attribute, $disabled_text, $form ) {
		// Get default country (fallback to 'us' if not set)
		$default_country = ! empty( $this->defaultCountry ) ? strtolower( $this->defaultCountry ) : 'us';

		// Parse value if it's JSON
		$phone_number = '';
		$country_iso = $default_country;
		if ( ! empty( $value ) && GFCommon::is_json( $value ) ) {
			$decoded = $this->to_array( $value );
			if ( $decoded && isset( $decoded['country'] ) ) {
				$country_iso = strtolower( $decoded['country'] );
				$phone_number = isset( $decoded['national'] ) ? $decoded['national'] : ( isset( $decoded['formatted'] ) ? $decoded['formatted'] : '' );
			}
		} elseif ( ! empty( $value ) ) {
			$phone_number = $value;
		}

		// Generate unique ID for this instance
		$unique_id = uniqid();
		$visible_input_id = $field_id . '_visible';
		$dropdown_id = 'gform_phone_dropdown_' . $unique_id;

		// Button ID for aria-controls
		$button_id = 'country_selector_button_' . $this->id;

		// Get country data (dial code and placeholder)
		$country_data = self::get_country_data( $country_iso );
		$dial_code    = $country_data['dialCode'];

		// Get sublabels
		$hidden_sublabel_class = $this->subLabelPlacement === 'hidden_label' ? 'hidden_sub_label screen-reader-text' : '';
		$country_sublabel_text = esc_html( $this->countrySublabel ?: __( 'Country', 'gravityforms' ) );
		$phone_sublabel_text   = esc_html( $this->phoneSublabel ?: __( 'Phone Number', 'gravityforms' ) );
		$country_sublabel      = "<label for='" . esc_attr( $button_id ) . "' id='" . esc_attr( $button_id . '_label' ) . "' class='gform-field-label gform-field-label--type-sub {$hidden_sublabel_class}'>{$country_sublabel_text}</label>";
		$phone_sublabel        = "<label for='" . esc_attr( $visible_input_id ) . "' id='" . esc_attr( $visible_input_id . '_label' ) . "' class='gform-field-label gform-field-label--type-sub {$hidden_sublabel_class}'>{$phone_sublabel_text}</label>";

		$is_sublabel_above = $this->is_sub_label_above( $form );

		$country_sublabel_above = $is_sublabel_above ? $country_sublabel : '';
		$country_sublabel_below = $is_sublabel_above ? '' : $country_sublabel;
		$phone_sublabel_above   = $is_sublabel_above ? $phone_sublabel : '';
		$phone_sublabel_below   = $is_sublabel_above ? '' : $phone_sublabel;

		// Use country-specific placeholder in form editor
		if ( $this->is_form_editor() && $this->phoneFormat === 'formatted' ) {
			$placeholder           = $country_data['placeholder'] ? $country_data['placeholder'] : '(201) 555-0123';
			$placeholder_attribute = sprintf( 'placeholder="%s"', esc_attr( $placeholder ) );
		}

		// Get i18n strings
		$i18n = array(
			'selectCountryAriaLabel' => __( 'Select country', 'gravityforms' ),
			'searchCountriesPlaceholder' => __( 'Search countries', 'gravityforms' ),
			'searchCountriesAriaLabel' => __( 'Search for a country', 'gravityforms' ),
			'countryListAriaLabel' => __( 'Country list', 'gravityforms' ),
		);
		$show_country_code = isset( $this->showCountryCode ) ? $this->showCountryCode : true;
		$wrapper_class = 'gform-phone';
		if ( $show_country_code ) {
			$wrapper_class .= ' gform-phone--show-dial-code';
		}

		$dial_code_display = $dial_code ? '+' . esc_attr( $dial_code ) : '';

		$html = sprintf( '
			<div class="%1$s" role="application">
				<div class="gform-phone__input-wrapper">
					<span class="ginput_country-selector_container">
						%2$s
						<button type="button" id="%3$s" class="gform-phone__country-selector gform-theme-button gform-theme-button--tertiary" aria-haspopup="listbox" aria-expanded="false" aria-controls="%4$s" aria-label="%5$s" %6$s>
							<span class="gform-phone__flag-icon gform-phone__flag-icon--%7$s"></span>
							<span class="gform-phone__dial-code">%8$s</span>
						</button>
						%9$s
					</span>
					<span class="ginput_phone_container">
						%10$s
						<input type="tel" class="gform-phone__input" id="%11$s" name="" autocomplete="tel" value="%12$s" %13$s %14$s %15$s %16$s %17$s %18$s>
						%19$s
					</span>
				</div>
				<div class="gform-phone__dropdown gform-phone__dropdown--hidden" role="listbox" id="%20$s" tabindex="-1">
					<div class="gform-phone__search-wrapper">
						<input type="text" class="gform-phone__search" placeholder="%21$s" aria-label="%22$s" aria-controls="%23$s_list">
					</div>
					<ul class="gform-phone__country-list gform-ul-reset" id="%24$s_list" role="listbox" aria-label="%25$s">
						<!-- Country items will be populated by JavaScript -->
					</ul>
					<div class="gform-phone__aria-live-search-status" aria-live="polite" aria-atomic="true" style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); clip-path: inset(50%%); white-space: nowrap;"></div>
				</div>
				<div class="gform-phone__aria-live" aria-live="polite" aria-atomic="true" style="position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%%); border: 0; white-space: nowrap;"></div>
			</div>',
			esc_attr( $wrapper_class ),                         // 1: wrapper class
			$country_sublabel_above,                            // 2: country sublabel above
			esc_attr( $button_id ),                             // 3: button id
			esc_attr( $dropdown_id ),                           // 4: aria-controls
			esc_attr( $i18n['selectCountryAriaLabel'] ),        // 5: aria-label
			$disabled_text,                                     // 6: disabled
			esc_attr( $country_iso ),                           // 7: flag icon class
			$dial_code_display,                                 // 8: dial code with + prefix or empty
			$country_sublabel_below,                            // 9: country sublabel below
			$phone_sublabel_above,                              // 10: phone sublabel above
			esc_attr( $visible_input_id ),                      // 11: visible input id
			esc_attr( $phone_number ),                          // 12: visible input value
			$placeholder_attribute,                             // 13
			$tabindex,                                          // 14
			$required_attribute,                                // 15
			$invalid_attribute,                                 // 16
			$aria_describedby,                                  // 17
			$disabled_text,                                     // 18
			$phone_sublabel_below,                              // 19: phone sublabel below
			esc_attr( $dropdown_id ),                           // 20: dropdown id
			esc_attr( $i18n['searchCountriesPlaceholder'] ),    // 21
			esc_attr( $i18n['searchCountriesAriaLabel'] ),      // 22
			esc_attr( $dropdown_id ),                           // 23: list id prefix
			esc_attr( $dropdown_id ),                           // 24: list id
			esc_attr( $i18n['countryListAriaLabel'] )           // 25
		);

		return $html;
	}

	/**
	 * Helper method to decode JSON phone value to array.
	 *
	 * @param string $value The JSON string to decode.
	 *
	 * @return array|false The decoded array or false if invalid JSON.
	 */
	private function to_array( $value ) {
		if ( ! GFCommon::is_json( $value ) ) {
			return false;
		}

		$decoded = json_decode( $value, true );
		return is_array( $decoded ) ? $decoded : false;
	}

	/**
	 * Gets the value of the submitted field.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @used-by GFFormsModel::get_field_value()
	 * @uses    GF_Field::get_value_submission()
	 * @uses    GF_Field_Phone::sanitize_entry_value()
	 *
	 * @param array $field_values             The dynamic population parameter names with their corresponding values to be populated.
	 * @param bool  $get_from_post_global_var Whether to get the value from the $_POST array as opposed to $field_values. Defaults to true.
	 *
	 * @return array|string
	 */
	public function get_value_submission( $field_values, $get_from_post_global_var = true ) {

		$value = parent::get_value_submission( $field_values, $get_from_post_global_var );
		$value = $this->sanitize_entry_value( $value, $this->formId );

		return $value;
	}

	/**
	 * Sanitizes the entry value.
	 *
	 * @since Unknown
	 * @access public
	 *
	 * @used-by GF_Field_Phone::get_value_save_entry()
	 * @used-by GF_Field_Phone::get_value_submission()
	 *
	 * @param string $value   The value to be sanitized.
	 * @param int    $form_id The form ID of the submitted item.
	 *
	 * @return array|array[]|string[] The sanitized value.
	 */
	public function sanitize_entry_value( $value, $form_id ) {
		if ( is_array( $value ) ) {
			return array_map( function( $v ) use ( $form_id ) {
				return $this->sanitize_entry_value( $v, $form_id );
			}, $value );
		}

		$sanitized = sanitize_text_field( $value );
		$this->post_entry_value_sanitization( $value, $sanitized, 'sanitize_text_field' );

		return $sanitized;
	}

	/**
	 * Sanitize and format the value before it is saved to the Entry Object.
	 *
	 * @since 3.0.0
	 *
	 * @param string $value          The value to be saved.
	 * @param array  $form           The Form object currently being processed.
	 * @param string $input_name     The input name used when accessing the $_POST.
	 * @param int    $entry_id        The ID of the entry currently being processed.
	 * @param array  $entry           The entry currently being processed.
	 * @param string $repeater_index The repeater index if the field is inside a repeater.
	 *
	 * @return array|string The sanitized and formatted input value to be saved.
	 */
	public function get_value_save_input( $value, $form, $input_name, $entry_id, $entry, $repeater_index = '' ) {
		$value = $this->sanitize_entry_value( $value, $form['id'] );

		if ( $this->phoneFormat == 'standard' && preg_match( '/^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$/', $value, $matches ) ) {
			$value = sprintf( '(%s) %s-%s', $matches[1], $matches[2], $matches[3] );
		} elseif ( $this->phoneFormat == 'formatted' && ! empty( $value ) ) {
			// Formatted international phone must be JSON
			$decoded = $this->to_array( $value );

			if ( ! $decoded ) {
				return ''; // Not valid JSON
			}

			// Define allowed keys
			$allowed_keys = array( 'country', 'national', 'formatted', 'e164' );

			// Check for extra keys
			$extra_keys = array_diff( array_keys( $decoded ), $allowed_keys );
			if ( ! empty( $extra_keys ) ) {
				return ''; // Reject if extra keys present
			}

			// Validate required fields
			foreach ( $allowed_keys as $key ) {
				if ( ! isset( $decoded[ $key ] ) ) {
					return ''; // Invalid structure
				}
			}

			// Validate country code
			if ( ! is_string( $decoded['country'] ) || strlen( $decoded['country'] ) !== 2 ) {
				return '';
			}

			// Validate E.164 format
			if ( ! preg_match( '/^\+[1-9]\d{1,14}$/', $decoded['e164'] ) ) {
				return '';
			}

			// Sanitize values
			$sanitized = array(
				'country'   => strtoupper( substr( $decoded['country'], 0, 2 ) ),
				'national'  => preg_replace( '/[^\d\s\-\(\).]/', '', $decoded['national'] ),
				'formatted' => wp_kses( $decoded['formatted'], array() ),
				'e164'      => preg_replace( '/[^\d\+]/', '', $decoded['e164'] )
			);

			$value = json_encode( $sanitized );
		}

		return $value;
	}

	/**
	 * Gets the value to be displayed on the entry detail page.
	 *
	 * @since: next
	 * @access public
	 *
	 * @param string $value    The field value.
	 * @param string $currency The entry currency code.
	 * @param bool   $use_text When processing merge tags, etc.
	 * @param string $format   The format requested for the location the merge is being used. Possible values: html, text or url.
	 * @param string $media    The location where the value will be displayed.
	 *
	 * @return string
	 */
	public function get_value_entry_detail( $value, $currency = '', $use_text = false, $format = 'html', $media = 'screen' ) {
		if ( ! empty( $value ) && GFCommon::is_json( $value ) ) {
			$decoded = $this->to_array( $value );
			if ( $decoded && isset( $decoded['formatted'] ) && isset( $decoded['e164'] ) && isset( $decoded['country'] ) ) {

				// Format: formatted value on first line, E.164 (country) on second line
				$line_break = $format == 'html' ? '<br />' : "\n";
				return $decoded['formatted'] . $line_break . __( 'Raw E.164 value:', 'gravityforms' ) . ' ' . $decoded['e164'] . ' (' . strtoupper( $decoded['country'] ) . ')';
			}
		}

		return $value;
	}

	/**
	 * Gets the value to be displayed on the entries list page.
	 *
	 * @since: next
	 * @access public
	 *
	 * @param string $value    The field value.
	 * @param array  $entry    The Entry Object.
	 * @param string $field_id The field ID.
	 * @param array  $columns  The columns to be displayed.
	 * @param array  $form     The Form Object.
	 *
	 * @return string
	 */
	public function get_value_entry_list( $value, $entry, $field_id, $columns, $form ) {
		if ( ! empty( $value ) && GFCommon::is_json( $value ) ) {
			$decoded = $this->to_array( $value );
			if ( $decoded && isset( $decoded['formatted'] ) ) {
				return $decoded['formatted'];
			}
		}

		return $value;
	}

	/**
	 * Gets the merge tag value.
	 *
	 * @since: next
	 * @access public
	 *
	 * @param string $value      The merge tag value to be filtered.
	 * @param string $input_id   The field or input ID from the merge tag.
	 * @param array  $entry      The Entry Object.
	 * @param array  $form       The Form Object.
	 * @param string $modifier   The merge tag modifier.
	 * @param string $raw_value  The raw field value from before formatting.
	 * @param bool   $url_encode Whether to encode the value for URLs.
	 * @param bool   $esc_html   Whether to encode HTML entities.
	 * @param string $format     The format requested for the location the merge is being used.
	 * @param bool   $nl2br      Whether to convert newlines to HTML line breaks.
	 *
	 * @return string
	 */
	public function get_value_merge_tag( $value, $input_id, $entry, $form, $modifier, $raw_value, $url_encode, $esc_html, $format, $nl2br ) {
		if ( $this->phoneFormat == 'formatted' && ! empty( $raw_value ) && GFCommon::is_json( $raw_value ) ) {
			$decoded = $this->to_array( $raw_value );
			if ( $decoded ) {
				// Support modifiers for object keys, like {Phone:1:country} or {Phone:1:e164}
				if ( $modifier && isset( $decoded[ $modifier ] ) ) {
					return $decoded[ $modifier ];
				}
				// Default to formatted value
				if ( isset( $decoded['formatted'] ) ) {
					return $decoded['formatted'];
				}
			}
		}

		return $value;
	}

	/**
	 * Gets the value to be used when exporting the entry.
	 *
	 * @since: next
	 * @access public
	 *
	 * @param array  $entry    The Entry Object.
	 * @param string $input_id The field or input ID.
	 * @param bool   $use_text Whether to use the text value.
	 * @param bool   $is_csv   Whether the export is for CSV.
	 *
	 * @return string
	 */
	public function get_value_export( $entry, $input_id = '', $use_text = false, $is_csv = false ) {
		if ( empty( $input_id ) ) {
			$input_id = $this->id;
		}

		$value = rgar( $entry, $input_id );

		if ( $this->phoneFormat == 'formatted' && ! empty( $value ) && GFCommon::is_json( $value ) ) {
			$decoded = $this->to_array( $value );
			if ( $decoded && isset( $decoded['e164'] ) ) {
				return isset( $decoded['e164'] ) ? $decoded['e164'] : $decoded['formatted'];
			}
		}

		return $value;
	}

	/**
	 * Sanitizes the field settings.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @used-by GFFormDetail::add_field()
	 * @used-by GFFormsModel::sanitize_settings()
	 * @uses    GF_Field::sanitize_settings()
	 * @uses    GF_Field_Phone::get_phone_format()
	 * @uses    GF_Field_Phone::$phoneFormat
	 *
	 * @return void
	 */
	public function sanitize_settings() {
		parent::sanitize_settings();

		if ( ! $this->get_phone_format() ) {
			$this->phoneFormat = 'formatted';
		}

		if ( $this->phoneFormat === 'formatted' ) {
			$this->storageType = 'json';
		}
	}

	/**
	 * Get an array of phone formats.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @used-by GF_Field_Phone::get_phone_format()
	 *
	 * @param null|int $form_id The ID of the current form or null to use the value from the current fields form_id property. Defaults to null.
	 *
	 * @return array The phone formats available.
	 */
	public function get_phone_formats( $form_id = null ) {

		if ( empty( $form_id ) ) {
			$form_id = $this->formId;
		}
		$form_id = absint( $form_id );

		$phone_formats = array(
			'formatted' => array(
				'label'       => __( 'International (formatted)', 'gravityforms' ),
				'mask'        => false,
				'regex'       => false,
				'instruction' => false,
			),
			'international' => array(
				'label'       => __( 'International (unformatted)', 'gravityforms' ),
				'mask'        => false,
				'regex'       => false,
				'instruction' => false,
			),
			'standard'      => array(
				'label'       => __( 'US Standard', 'gravityforms' ),
				'mask'        => '(999) 999-9999',
				'regex'       => '/^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$/',
				'instruction' => '(###) ###-####',
			),
		);

		/**
		 * Allow custom phone formats to be defined.
		 *
		 * @since 2.0.0
		 *
		 * @param array $phone_formats The phone formats.
		 * @param int   $form_id       The ID of the current form.
		 */
		return gf_apply_filters( array( 'gform_phone_formats', $form_id ), $phone_formats, $form_id );
	}

	/**
	 * Get the properties for the fields selected phone format.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @used-by GF_Field_Phone::get_field_input()
	 * @used-by GF_Field_Phone::sanitize_settings()
	 * @used-by GF_Field_Phone::validate()
	 * @uses    GF_Field_Phone::get_phone_formats()
	 * @uses    GF_Field_Phone::$phoneFormat
	 *
	 * @return array The phone format.
	 */
	public function get_phone_format() {
		$phone_formats = $this->get_phone_formats();

		return rgar( $phone_formats, $this->phoneFormat );
	}

	/**
	 * Actions to be performed after the field has been converted to an object.
	 *
	 * @since 3.0.2
	 *
	 * @return void
	 */
	public function post_convert_field() {
		parent::post_convert_field();
		if ( ! $this->get_phone_format() ) {
			$this->phoneFormat = 'formatted';
		}
	}

}

// Register the phone field with the field framework.
GF_Fields::register( new GF_Field_Phone() );