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:
kate522 committed 2026-07-27 15:39:16 +08:00
1 parent c206928746
commit 59f5d5d98a
12 files changed
+256 -54

No files matched your search

+9 -4
View File
@@ -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);
}
+56 -4
View File
@@ -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',