fix(ip-geo): harden full-chain IP geolocation reliability
Capture real client IPs behind CDN, improve provider fallbacks and negative cache, fix admin credential dependsOn for polling mode, and map country labels to ISO codes for moderation GeoIP blocks.
This commit is contained in:
1 parent
c206928746
commit
59f5d5d98a
12 files changed
+256
-54
No files matched your search
@@ -127,14 +127,19 @@ class APIAudit {
|
||||
* Get client IP
|
||||
*/
|
||||
private function get_client_ip(): string {
|
||||
if (function_exists('aksrt_get_client_ip')) {
|
||||
return sanitize_text_field((string) aksrt_get_client_ip());
|
||||
}
|
||||
$ip = '';
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
|
||||
$ip = (string) $_SERVER['HTTP_CF_CONNECTING_IP'];
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$parts = explode(',', (string) $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
$ip = trim($parts[0]);
|
||||
} elseif (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
||||
$ip = $_SERVER['HTTP_X_REAL_IP'];
|
||||
$ip = (string) $_SERVER['HTTP_X_REAL_IP'];
|
||||
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
$ip = (string) $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
return sanitize_text_field($ip);
|
||||
}
|
||||
|
||||
@@ -45,19 +45,31 @@ class IPGeo {
|
||||
|
||||
$cache_key = 'aksrt_ip_geo_' . md5($ip);
|
||||
$ttl = max(3600, (int) ($config['ip_geo_cache_ttl'] ?? 86400));
|
||||
// Negative cache: avoid hammering providers on repeated failures (admin force skips).
|
||||
$neg_key = 'aksrt_ip_geo_neg_' . md5($ip);
|
||||
$neg_ttl = 600;
|
||||
|
||||
if (!$force) {
|
||||
$cached = get_transient($cache_key);
|
||||
if ($cached !== false && is_array($cached)) {
|
||||
return $cached;
|
||||
}
|
||||
$neg = get_transient($neg_key);
|
||||
if ($neg !== false && is_array($neg)) {
|
||||
return $neg;
|
||||
}
|
||||
}
|
||||
|
||||
$provider = sanitize_key((string) ($config['ip_addr_sdk'] ?? 'null'));
|
||||
$result = $this->query_provider($provider, $ip, $config);
|
||||
|
||||
if ($this->has_location($result)) {
|
||||
// Strip bulky raw payloads before caching.
|
||||
$store = $result;
|
||||
unset($store['raw']);
|
||||
if (!$force) {
|
||||
set_transient($cache_key, $result, $ttl);
|
||||
set_transient($cache_key, $store, $ttl);
|
||||
delete_transient($neg_key);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
@@ -66,6 +78,13 @@ class IPGeo {
|
||||
$result['reason'] = 'provider_empty';
|
||||
}
|
||||
|
||||
// Cache miss with reason so front-end doesn't retry every page view.
|
||||
if (!$force) {
|
||||
$store = $result;
|
||||
unset($store['raw']);
|
||||
set_transient($neg_key, $store, $neg_ttl);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -85,11 +104,16 @@ class IPGeo {
|
||||
$missing[] = 'ip_addr_sdk_amap_appkey';
|
||||
}
|
||||
}
|
||||
// polling / null (free): no hard credential requirements.
|
||||
|
||||
// Free + polling are always "configured" once enabled; paid need keys.
|
||||
// verified flips after a successful admin test (or any write of ip_geo_verified).
|
||||
$configured = $enabled && empty($missing);
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'provider' => $provider,
|
||||
'configured' => $enabled && empty($missing),
|
||||
'configured' => $configured,
|
||||
'available' => $enabled,
|
||||
'verified' => !empty($config['ip_geo_verified']),
|
||||
'transport' => 'http_api',
|
||||
@@ -110,11 +134,19 @@ class IPGeo {
|
||||
$province = $this->string_field($geo['province'] ?? '');
|
||||
$city = $this->string_field($geo['city'] ?? '');
|
||||
|
||||
if ($province !== '' && $city !== '' && $province === $city) {
|
||||
$city = '';
|
||||
}
|
||||
|
||||
switch ($format) {
|
||||
case 'province':
|
||||
return $province !== '' ? $province : $city;
|
||||
if ($province !== '') {
|
||||
return $province;
|
||||
}
|
||||
return $city !== '' ? $city : $country;
|
||||
case 'country_province':
|
||||
return trim($country . ' ' . $province);
|
||||
$joined = trim($country . ' ' . $province);
|
||||
return $joined !== '' ? $joined : $city;
|
||||
case 'province_city':
|
||||
default:
|
||||
$parts = array_filter([$province, $city]);
|
||||
@@ -255,6 +287,19 @@ class IPGeo {
|
||||
]);
|
||||
}
|
||||
|
||||
// Amap IP API is IPv4-only; free path for IPv6.
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$free = $this->query_ip_api($ip);
|
||||
if ($this->has_location($free)) {
|
||||
$free['detail'] = 'amap_ipv6_fallback_ip_api';
|
||||
return $free;
|
||||
}
|
||||
return $this->empty_result('provider_empty', [
|
||||
'provider' => 'amap',
|
||||
'detail' => 'amap_ipv4_only',
|
||||
]);
|
||||
}
|
||||
|
||||
// Amap sign: md5(sorted query without sig + secret) — zibll uses ip=&key= order.
|
||||
$base_query = 'ip=' . $ip . '&key=' . $appkey;
|
||||
$url = 'https://restapi.amap.com/v3/ip?' . $base_query;
|
||||
@@ -374,6 +419,13 @@ class IPGeo {
|
||||
$country = $this->string_field($addr[1] ?? '');
|
||||
|
||||
if ($province === '' && $city_s === '') {
|
||||
// HTTP OK but unusable body (block page / CAPTCHA / empty) → free path.
|
||||
$fallback = $this->query_ip_api($ip);
|
||||
if ($this->has_location($fallback)) {
|
||||
$fallback['provider'] = 'ip-api';
|
||||
$fallback['detail'] = 'pconline_parse_failed_fallback_ip_api';
|
||||
return $fallback;
|
||||
}
|
||||
return $this->empty_result('provider_empty', [
|
||||
'provider' => 'pconline',
|
||||
'detail' => 'parse_failed',
|
||||
|
||||
Reference in new issue
Block a user