PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.8
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.8
2.12.8 2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 All 98 releases
← All changes | inc/fields/phone-markup.php +21 -104 2.10.0 → 2.12.8 View file →
@@ -86,10 +86,15 @@
86 86 $this->country_filter_type = $attributes['countryFilterType'] ?? 'include';
87 87 $this->include_countries = $attributes['includeCountries'] ?? [];
88 88 $this->exclude_countries = $attributes['excludeCountries'] ?? [];
89 89
90 - // When auto country is enabled, detect the visitor's country via server-side
91 - // IP geolocation (ipapi.co) instead of a client-side fetch.
90 + // When auto country is enabled, resolve a best-effort country here at render
91 + // time to seed the baked `default-country` attribute (a sensible flag before
92 + // any JS runs). The authoritative per-visitor detection happens client-side:
93 + // phone.js applies an immediate network-free Intl guess and then refines it
94 + // from the same-origin geo-country REST endpoint — so it stays correct even on
95 + // full-page-cached sites, where this baked value would otherwise be the first
96 + // visitor's country.
92 97 //
93 98 // Why not get_locale()?
94 99 // WordPress get_locale() returns the *site's* configured language (e.g. 'en_US'),
95 100 // not the visitor's physical location. A site set to English would show 'US' for
@@ -94,17 +99,20 @@
94 99 // WordPress get_locale() returns the *site's* configured language (e.g. 'en_US'),
95 100 // not the visitor's physical location. A site set to English would show 'US' for
96 101 // every visitor worldwide — defeating the purpose of auto-country detection.
97 102 //
98 - // Why server-side instead of client-side?
99 - // The previous client-side fetch('https://ipapi.co/json') caused CORS failures,
100 - // 429 rate limits on high-traffic sites, and exposed visitor IPs to a third party
101 - // directly from the browser. Moving it server-side eliminates all three issues.
102 - //
103 - // Performance: The API is called only once per visitor IP and cached in a transient
104 - // for 24 hours — subsequent page loads for the same IP resolve instantly from cache.
103 + // Why resolve server-side at all (vs. a browser geo-IP fetch)?
104 + // A client-side fetch('https://ipapi.co/json') caused CORS failures, 429 rate
105 + // limits on high-traffic sites, and exposed visitor IPs to a third party from the
106 + // browser. The server path (CDN header first, then a capped, cached ipapi.co
107 + // lookup in Helper::get_geo_country()) avoids all three.
105 108 if ( $this->auto_country ) {
106 - $this->default_country = $this->get_geo_country();
109 + // Pass the configured default as the fallback so a detection failure
110 + // degrades to the user's chosen country instead of a hardcoded 'us'.
111 + $fallback = ! empty( $this->default_country ) && is_string( $this->default_country )
112 + ? strtolower( $this->default_country )
113 + : 'us';
114 + $this->default_country = Helper::get_geo_country( $fallback );
107 115 }
108 116 $this->set_unique_slug();
109 117 $this->set_field_name( $this->unique_slug );
110 118 $this->set_markup_properties( $this->input_label, true );
@@ -131,8 +139,11 @@
131 139 <?php echo ! empty( $this->aria_described_by ) ? "aria-describedby='" . esc_attr( trim( $this->aria_described_by ) ) . "'" : ''; ?>
132 140 data-required="<?php echo esc_attr( $this->data_require_attr ); ?>"
133 141 aria-required="<?php echo esc_attr( $this->data_require_attr ); ?>"
134 142 default-country="<?php echo esc_attr( $this->default_country ); ?>"
143 + <?php if ( $this->auto_country ) { ?>
144 + data-auto-country="true"
145 + <?php } ?>
135 146 <?php if ( $this->enable_country_filter ) { ?>
136 147 data-enable-country-filter="true"
137 148 data-country-filter-type="<?php echo esc_attr( $this->country_filter_type ); ?>"
138 149 <?php if ( 'include' === $this->country_filter_type && ! empty( $this->include_countries ) ) { ?>
@@ -152,99 +163,5 @@
152 163 <?php
153 164 return ob_get_clean();
154 165 }
155 166
156 - /**
157 - * Detect the visitor's 2-letter country code via server-side IP geolocation.
158 - *
159 - * Calls ipapi.co once per visitor IP and caches the result in a transient for
160 - * 24 hours so subsequent page loads resolve instantly without any API call.
161 - *
162 - * Failure responses (network error, non-200, malformed body, invalid country code)
163 - * are also cached as 'us' for 1 hour to prevent a thundering-herd retry storm
164 - * if ipapi.co goes down or rate-limits us.
165 - *
166 - * Private/reserved IPs (e.g., 10.x, 192.168.x, 127.0.0.1, ::1) are rejected up
167 - * front — ipapi.co cannot geolocate them, and accepting them would let spoofed
168 - * X-Forwarded-For headers flood the transient cache.
169 - *
170 - * A site-wide hourly cap (default 40, filterable via `srfm_geo_api_hourly_cap`)
171 - * bounds outbound calls so a determined attacker can't exhaust the ipapi free-tier
172 - * quota (1,000/day) by rotating spoofed public IPs.
173 - *
174 - * Note on page caching: When a full-page cache plugin is active, the HTML
175 - * (including default-country) is served from cache. The first visitor's country
176 - * is baked into the cached page. This is an acceptable tradeoff — the alternative
177 - * (client-side fetch) caused CORS failures and 429 rate limits. Sites needing
178 - * per-visitor precision can set a specific default country per field.
179 - *
180 - * @since 2.8.0
181 - * @return string Lowercase 2-letter country code, defaults to 'us'.
182 - */
183 - private function get_geo_country() {
184 - $ip = Helper::get_visitor_ip();
185 - if ( empty( $ip ) ) {
186 - return 'us';
187 - }
188 -
189 - // Reject private/reserved IPs: ipapi.co returns "Reserved IP Address" for them,
190 - // and accepting them would let spoofed X-Forwarded-For headers flood the cache.
191 - if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
192 - return 'us';
193 - }
194 -
195 - $cache_key = 'srfm_geo_' . md5( $ip );
196 - $cached = get_transient( $cache_key );
197 - if ( is_string( $cached ) && '' !== $cached ) {
198 - return $cached;
199 - }
200 -
201 - // Site-wide hourly cap on outbound ipapi calls. The counter rolls over every hour
202 - // (key includes YmdH) so we never need to explicitly reset it. Default 40 stays
203 - // well under ipapi's 1,000/day free tier; paid-tier sites can raise via filter.
204 - $quota_key = 'srfm_geo_quota_' . gmdate( 'YmdH' );
205 - $quota_cap = Helper::get_integer_value( apply_filters( 'srfm_geo_api_hourly_cap', 40 ) );
206 - $count = Helper::get_integer_value( get_transient( $quota_key ) );
207 - if ( $count >= $quota_cap ) {
208 - set_transient( $cache_key, 'us', HOUR_IN_SECONDS );
209 - return 'us';
210 - }
211 - set_transient( $quota_key, $count + 1, HOUR_IN_SECONDS );
212 -
213 - // ipapi.co's /json/ endpoint geolocates the *caller's* IP. Since this request
214 - // originates from the WordPress server (not the visitor's browser), we must
215 - // pass the visitor's IP explicitly via /{ip}/json/ — otherwise ipapi.co
216 - // returns the hosting datacenter's country for every visitor.
217 - $url = 'https://ipapi.co/' . rawurlencode( $ip ) . '/json/';
218 - $response = wp_remote_get(
219 - $url,
220 - [
221 - 'timeout' => 3,
222 - 'user-agent' => 'SureForms/' . SRFM_VER . ' (+https://sureforms.com)',
223 - ]
224 - );
225 -
226 - if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
227 - set_transient( $cache_key, 'us', HOUR_IN_SECONDS );
228 - return 'us';
229 - }
230 -
231 - $body = json_decode( wp_remote_retrieve_body( $response ), true );
232 -
233 - if ( ! is_array( $body ) || empty( $body['country_code'] ) || ! is_string( $body['country_code'] ) ) {
234 - set_transient( $cache_key, 'us', HOUR_IN_SECONDS );
235 - return 'us';
236 - }
237 -
238 - $country = strtolower( $body['country_code'] );
239 -
240 - // Validate the external API response is a valid 2-letter country code.
241 - if ( ! preg_match( '/^[a-z]{2}$/', $country ) ) {
242 - set_transient( $cache_key, 'us', HOUR_IN_SECONDS );
243 - return 'us';
244 - }
245 -
246 - set_transient( $cache_key, $country, DAY_IN_SECONDS );
247 -
248 - return $country;
249 - }
250 167 }