'phone_mode', 'type' => 'select', 'label' => __( 'Phone field type', '2ranq-forms' ), 'help' => __( 'Smart adds a country code in front of the field and checks how many digits that country uses.', '2ranq-forms' ), 'default' => 'plain', 'options' => array( 'plain' => __( 'Plain phone field', '2ranq-forms' ), 'smart' => __( 'Smart, with country code', '2ranq-forms' ), ), ); $countries = array(); foreach ( Ranq_Forms_Phone_Countries::all() as $code => $country ) { $countries[ $code ] = $country['name'] . ' (+' . $country['dial'] . ')'; } $settings['advanced'][] = array( 'key' => 'phone_country', 'type' => 'select', 'label' => __( 'Starting country', '2ranq-forms' ), 'help' => __( 'Smart field only. Used when the visitor country cannot be worked out from the request or from the site language.', '2ranq-forms' ), 'default' => Ranq_Forms_Phone_Countries::DEFAULT_COUNTRY, 'options' => $countries, ); return $settings; } /** * Whether the field is in smart mode. * * @param array $field The field settings. * * @return bool */ private function is_smart( $field ) { return isset( $field['phone_mode'] ) && 'smart' === $field['phone_mode']; } /** * Guesses the visitor's country. * * 🔴 Nothing leaves the site. There is no call to somebody else's service to * resolve a country from an address, because that would mean sending the * visitor's address to a third party, and the WordPress.org machine check * rejects such a submission anyway. * * The order, and why exactly this one: * * 1. The `CF-IPCountry` header. The only thing that knows where THIS visitor * is. It arrives only when the site sits behind Cloudflare, and it costs * no request at all. * 2. The site language. It is the same for every visitor, so it is weaker * evidence, but better than guessing. It is used ONLY when somebody * actually picked the language: `en_US` is the value a site has when * nobody touched anything, so it says nothing about the visitor and is * skipped. * 3. What the owner picked in the field settings. It comes last because it is * their assumption, while the first two are a measurement. * * ⚠️ When the page sits in a cache, the first step holds for whoever opened * the page first. That is why this is NOT a check but only a starting value: * the visitor changes the country with one click, and the server validates * what was actually sent. * * @param array $field The field settings. * * @return string The country letter code. */ private function guess_country( $field ) { // 1. Cloudflare. $header = isset( $_SERVER['HTTP_CF_IPCOUNTRY'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_IPCOUNTRY'] ) ) : ''; if ( '' !== $header && Ranq_Forms_Phone_Countries::exists( $header ) ) { return Ranq_Forms_Phone_Countries::normalize_code( $header ); } // 2. The site language. $locale = get_locale(); if ( 'en_US' !== $locale ) { $code = $this->country_from_locale( $locale ); if ( '' !== $code ) { return $code; } } // 3. What the owner picked. $chosen = isset( $field['phone_country'] ) ? (string) $field['phone_country'] : ''; if ( Ranq_Forms_Phone_Countries::exists( $chosen ) ) { return Ranq_Forms_Phone_Countries::normalize_code( $chosen ); } return Ranq_Forms_Phone_Countries::DEFAULT_COUNTRY; } /** * The country out of a locale, for example `sr_RS` into `RS`. * * A language without a country (`de`) is resolved through a small list, * because guessing Germany is better than leaving the person on America. * * @param string $locale The site locale. * * @return string Empty when nothing can be concluded. */ private function country_from_locale( $locale ) { $locale = str_replace( '-', '_', (string) $locale ); $parts = explode( '_', $locale ); if ( isset( $parts[1] ) && Ranq_Forms_Phone_Countries::exists( $parts[1] ) ) { return Ranq_Forms_Phone_Countries::normalize_code( $parts[1] ); } $by_language = array( 'ar' => 'AE', 'bg' => 'BG', 'bs' => 'BA', 'cs' => 'CZ', 'da' => 'DK', 'de' => 'DE', 'el' => 'GR', 'es' => 'ES', 'et' => 'EE', 'fi' => 'FI', 'fr' => 'FR', 'he' => 'IL', 'hr' => 'HR', 'hu' => 'HU', 'id' => 'ID', 'it' => 'IT', 'ja' => 'JP', 'ko' => 'KR', 'lt' => 'LT', 'lv' => 'LV', 'mk' => 'MK', 'nb' => 'NO', 'nl' => 'NL', 'nn' => 'NO', 'no' => 'NO', 'pl' => 'PL', 'pt' => 'PT', 'ro' => 'RO', 'ru' => 'RU', 'sk' => 'SK', 'sl' => 'SI', 'sq' => 'AL', 'sr' => 'RS', 'sv' => 'SE', 'th' => 'TH', 'tr' => 'TR', 'uk' => 'UA', 'vi' => 'VN', 'zh' => 'CN', ); $language = strtolower( isset( $parts[0] ) ? $parts[0] : '' ); return isset( $by_language[ $language ] ) ? $by_language[ $language ] : ''; } /** * Prints the field. * * @param array $field The field settings. * @param int $form_id The form id. * * @return void */ public function render( $field, $form_id ) { if ( ! $this->is_smart( $field ) ) { parent::render( $field, $form_id ); return; } $describedby = $this->describedby( $field, $form_id ); $id = $this->input_id( $field, $form_id ); $name = $this->input_name( $field ); $guess = $this->guess_country( $field ); // After a failed send the visitor's country and number come back. sanitize() // always fills the country in, so an empty one only means a fresh form. $sticky = $this->sticky_value( $field, $form_id ); $country = is_array( $sticky ) && ! empty( $sticky['country'] ) ? $sticky['country'] : $guess; $number = is_array( $sticky ) && isset( $sticky['number'] ) ? (string) $sticky['number'] : ( empty( $field['default_value'] ) ? '' : (string) $field['default_value'] ); ?>