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:
@@ -172,9 +172,21 @@ function matchDependsOn(dependsOn) {
|
||||
return !!props.formData[dependsOn]
|
||||
}
|
||||
if (typeof dependsOn === 'object') {
|
||||
const { key, value, not } = dependsOn
|
||||
const { key, value, values, not } = dependsOn
|
||||
const currentVal = props.formData[key]
|
||||
if (not) return currentVal !== value
|
||||
if (not) {
|
||||
if (Array.isArray(values)) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return !values.some((v) => currentVal == v)
|
||||
}
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return currentVal !== value && currentVal != value
|
||||
}
|
||||
// values: match any of a list (e.g. provider ∈ [qq, polling])
|
||||
if (Array.isArray(values)) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return values.some((v) => currentVal == v)
|
||||
}
|
||||
// Loose equality so true matches 1 / '1' from some option stores.
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return currentVal == value || (!!value === true && !!currentVal)
|
||||
|
||||
@@ -44,10 +44,10 @@ export const ipSchema = {
|
||||
title: '接口凭据',
|
||||
icon: 'fas fa-key',
|
||||
fields: [
|
||||
{ key: 'ip_addr_sdk_qq_appkey', label: '腾讯 AppKey', type: 'input', default: '', dependsOn: { key: 'ip_addr_sdk', value: 'qq' } },
|
||||
{ key: 'ip_addr_sdk_qq_secretkey', label: '腾讯 SecretKey', type: 'input', default: '', dependsOn: { key: 'ip_addr_sdk', value: 'qq' } },
|
||||
{ key: 'ip_addr_sdk_amap_appkey', label: '高德 AppKey', type: 'input', default: '', dependsOn: { key: 'ip_addr_sdk', value: 'amap' }, help: '必须在高德开放平台创建「Web服务」类型 Key,并开通 IP 定位。选错平台会报 USERKEY_PLAT_NOMATCH。' },
|
||||
{ key: 'ip_addr_sdk_amap_secretkey', label: '高德 SecretKey', type: 'input', default: '', dependsOn: { key: 'ip_addr_sdk', value: 'amap' }, help: '若控制台启用了「数字签名」,必填安全密钥;未启用可留空。' },
|
||||
{ key: 'ip_addr_sdk_qq_appkey', label: '腾讯 AppKey', type: 'input', default: '', dependsOn: { key: 'ip_addr_sdk', values: ['qq', 'polling'] }, help: '「腾讯位置服务」或「轮流查询」时填写;未填则轮询会跳过腾讯。' },
|
||||
{ key: 'ip_addr_sdk_qq_secretkey', label: '腾讯 SecretKey', type: 'input', default: '', dependsOn: { key: 'ip_addr_sdk', values: ['qq', 'polling'] }, help: '启用 SK 签名时必填;未启用可留空。' },
|
||||
{ key: 'ip_addr_sdk_amap_appkey', label: '高德 AppKey', type: 'input', default: '', dependsOn: { key: 'ip_addr_sdk', values: ['amap', 'polling'] }, help: '必须在高德开放平台创建「Web服务」类型 Key,并开通 IP 定位。选错平台会报 USERKEY_PLAT_NOMATCH。「轮流查询」时未填则跳过高德。' },
|
||||
{ key: 'ip_addr_sdk_amap_secretkey', label: '高德 SecretKey', type: 'input', default: '', dependsOn: { key: 'ip_addr_sdk', values: ['amap', 'polling'] }, help: '若控制台启用了「数字签名」,必填安全密钥;未启用可留空。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -151,18 +151,34 @@ if (!function_exists('aksrt_format_geo_label')) :
|
||||
return $label;
|
||||
}
|
||||
}
|
||||
$country = $addr['country'];
|
||||
$province = $addr['province'];
|
||||
$city = $addr['city'];
|
||||
|
||||
// Drop duplicated province+city when provider returns the same token twice.
|
||||
if ($province !== '' && $city !== '' && $province === $city) {
|
||||
$city = '';
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'province':
|
||||
return $addr['province'] !== '' ? $addr['province'] : $addr['city'];
|
||||
if ($province !== '') {
|
||||
return $province;
|
||||
}
|
||||
return $city !== '' ? $city : $country;
|
||||
case 'country_province':
|
||||
return trim($addr['country'] . ' ' . $addr['province']);
|
||||
$joined = trim($country . ' ' . $province);
|
||||
if ($joined !== '') {
|
||||
return $joined;
|
||||
}
|
||||
return $city;
|
||||
case 'province_city':
|
||||
default:
|
||||
$parts = array_filter([$addr['province'], $addr['city']]);
|
||||
if (empty($parts) && $addr['country'] !== '') {
|
||||
return $addr['country'];
|
||||
$parts = array_filter([$province, $city]);
|
||||
if (!empty($parts)) {
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
return implode(' ', $parts);
|
||||
return $country;
|
||||
}
|
||||
}
|
||||
endif;
|
||||
|
||||
@@ -50,6 +50,21 @@ if (!function_exists('aksrt_store_comment_addr')) :
|
||||
$ip = $comment instanceof WP_Comment ? (string) $comment->comment_author_IP : '';
|
||||
}
|
||||
$ip = trim((string) $ip);
|
||||
|
||||
// Private / empty stored IP (CDN edge without proxy headers): resolve live client IP once.
|
||||
$needs_live = ($ip === '' || $ip === '0.0.0.0');
|
||||
if (!$needs_live && function_exists('filter_var')) {
|
||||
$needs_live = !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
|
||||
}
|
||||
if ($needs_live && function_exists('aksrt_get_client_ip')) {
|
||||
$live = trim((string) aksrt_get_client_ip());
|
||||
if ($live !== '' && $live !== '0.0.0.0'
|
||||
&& filter_var($live, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)
|
||||
) {
|
||||
$ip = $live;
|
||||
}
|
||||
}
|
||||
|
||||
if ($ip === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
+29
-4
@@ -218,11 +218,29 @@ function aksrt_get_captcha_status_summary(): array {
|
||||
// ============================================================
|
||||
|
||||
function aksrt_get_client_ip(): string {
|
||||
$keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'];
|
||||
// Prefer CDN / reverse-proxy headers (Cloudflare first), then generic proxies.
|
||||
$keys = [
|
||||
'HTTP_CF_CONNECTING_IP',
|
||||
'HTTP_TRUE_CLIENT_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_REAL_IP',
|
||||
'HTTP_CLIENT_IP',
|
||||
];
|
||||
foreach ($keys as $key) {
|
||||
if (!empty($_SERVER[$key])) {
|
||||
$parts = explode(',', (string) $_SERVER[$key]);
|
||||
$ip = trim($parts[0]);
|
||||
if (empty($_SERVER[$key])) {
|
||||
continue;
|
||||
}
|
||||
// X-Forwarded-For may be a chain: client, proxy1, proxy2 — take first valid.
|
||||
$parts = explode(',', (string) $_SERVER[$key]);
|
||||
foreach ($parts as $part) {
|
||||
$ip = trim($part);
|
||||
// Strip IPv6 brackets / port noise occasionally injected by proxies.
|
||||
if (str_starts_with($ip, '[') && str_contains($ip, ']')) {
|
||||
$ip = substr($ip, 1, strpos($ip, ']') - 1);
|
||||
} elseif (substr_count($ip, ':') === 1 && str_contains($ip, '.')) {
|
||||
// IPv4:port
|
||||
$ip = explode(':', $ip, 2)[0];
|
||||
}
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return $ip;
|
||||
}
|
||||
@@ -231,6 +249,13 @@ function aksrt_get_client_ip(): string {
|
||||
return isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
|
||||
}
|
||||
|
||||
// Native wp-comments-post + any path that omits comment_author_IP.
|
||||
// Ensures comment_author_IP (and thus aksrt_comment_addr) is the real client IP behind CDN.
|
||||
add_filter('pre_comment_user_ip', static function ($ip) {
|
||||
$resolved = aksrt_get_client_ip();
|
||||
return ($resolved !== '' && $resolved !== '0.0.0.0') ? $resolved : $ip;
|
||||
}, 5);
|
||||
|
||||
// ============================================================
|
||||
// 腾讯云 API 3.0 TC3-HMAC-SHA256 签名
|
||||
// ============================================================
|
||||
|
||||
@@ -310,6 +310,19 @@ class ExtensionController extends ControllerBase {
|
||||
$message = '';
|
||||
$ok = !empty($result['country']) || !empty($result['province']) || !empty($result['city']);
|
||||
|
||||
// Persist "verified" once a live lookup succeeds so overview cards leave "待补全".
|
||||
if ($ok) {
|
||||
$all = \AKSRT\Settings\Settings::get_all();
|
||||
if (empty($all['ip_geo_verified'])) {
|
||||
$all['ip_geo_verified'] = true;
|
||||
update_option('aksrt_settings', $all);
|
||||
if (method_exists('\AKSRT\Settings\Settings', 'flush_settings_cache')) {
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
}
|
||||
$status = $geo->get_status();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$ok) {
|
||||
$detail = trim((string) ($result['detail'] ?? ''));
|
||||
switch ($reason) {
|
||||
|
||||
@@ -27,8 +27,8 @@ class SchemaExtensionsIP {
|
||||
['key' => 'ip_geo_cache_ttl', 'label' => '缓存时间 (秒)', 'type' => 'number', 'default' => 86400, 'min' => 3600, 'max' => 604800],
|
||||
]],
|
||||
['key' => 'providers', 'title' => '接口凭据', 'fields' => [
|
||||
['key' => 'ip_addr_sdk_qq_appkey', 'label' => '腾讯 AppKey', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'ip_addr_sdk_qq_secretkey', 'label' => '腾讯 SecretKey', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'ip_addr_sdk_qq_appkey', 'label' => '腾讯 AppKey', 'type' => 'input', 'default' => '', 'help' => '「腾讯位置服务」或「轮流查询」时填写'],
|
||||
['key' => 'ip_addr_sdk_qq_secretkey', 'label' => '腾讯 SecretKey', 'type' => 'input', 'default' => '', 'help' => '启用 SK 签名时必填'],
|
||||
['key' => 'ip_addr_sdk_amap_appkey', 'label' => '高德 AppKey', 'type' => 'input', 'default' => '', 'help' => '须为「Web服务」Key,并开通 IP 定位'],
|
||||
['key' => 'ip_addr_sdk_amap_secretkey', 'label' => '高德 SecretKey', 'type' => 'input', 'default' => '', 'help' => '启用数字签名时必填'],
|
||||
]],
|
||||
|
||||
@@ -92,25 +92,76 @@ trait ModerationPart2 {
|
||||
}
|
||||
|
||||
private function lookup_geoip(string $ip, array $config): ?string {
|
||||
// 优先复用 IP 归属地模块的查询能力(含缓存)
|
||||
if (class_exists('\AKSRT\Extensions\IPGeo')) {
|
||||
$result = \AKSRT\Extensions\IPGeo::get_instance()->lookup($ip);
|
||||
if (!empty($result['country'])) {
|
||||
return $result['country'];
|
||||
// Dedicated ISO country-code path for block lists (CN/US/…).
|
||||
// IPGeo returns localized names ("中国") which are not ISO codes.
|
||||
$provider = sanitize_key((string) ($config['geoip_provider'] ?? 'ipapi'));
|
||||
$api_key = (string) ($config['geoip_api_key'] ?? '');
|
||||
|
||||
if ($provider === 'ipinfo' || $provider === 'ipinfo.io') {
|
||||
$code = $this->geoip_ipinfo($ip, $api_key);
|
||||
if ($code !== null) {
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到内置查询
|
||||
$ip_geo_config = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_ip_geo_settings()
|
||||
: [];
|
||||
$provider = (string) ($ip_geo_config['ip_geo_provider'] ?? $config['geoip_provider'] ?? 'ip-api.com');
|
||||
$api_key = (string) ($ip_geo_config['ip_geo_api_key'] ?? $config['geoip_api_key'] ?? '');
|
||||
|
||||
if ($provider === 'ipinfo' || $provider === 'ipinfo.io') {
|
||||
return $this->geoip_ipinfo($ip, $api_key);
|
||||
$code = $this->geoip_ipapi($ip);
|
||||
if ($code !== null) {
|
||||
return $code;
|
||||
}
|
||||
return $this->geoip_ipapi($ip);
|
||||
|
||||
// Last resort: map IPGeo country label → ISO when possible.
|
||||
if (class_exists('\AKSRT\Extensions\IPGeo')) {
|
||||
$result = \AKSRT\Extensions\IPGeo::get_instance()->lookup($ip);
|
||||
$label = trim((string) ($result['country'] ?? ''));
|
||||
if ($label !== '') {
|
||||
return $this->country_label_to_iso($label);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map common country labels / ISO aliases to ISO-3166 alpha-2.
|
||||
*/
|
||||
private function country_label_to_iso(string $label): ?string {
|
||||
$raw = trim($label);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
if (preg_match('/^[A-Za-z]{2}$/', $raw)) {
|
||||
return strtoupper($raw);
|
||||
}
|
||||
$map = [
|
||||
'中国' => 'CN', '中华人民共和国' => 'CN', 'china' => 'CN', 'cn' => 'CN',
|
||||
'美国' => 'US', '美利坚合众国' => 'US', 'united states' => 'US', 'usa' => 'US', 'us' => 'US',
|
||||
'日本' => 'JP', 'japan' => 'JP', 'jp' => 'JP',
|
||||
'韩国' => 'KR', '大韩民国' => 'KR', 'south korea' => 'KR', 'korea' => 'KR', 'kr' => 'KR',
|
||||
'香港' => 'HK', '中国香港' => 'HK', 'hong kong' => 'HK', 'hk' => 'HK',
|
||||
'澳门' => 'MO', '中国澳门' => 'MO', 'macao' => 'MO', 'macau' => 'MO', 'mo' => 'MO',
|
||||
'台湾' => 'TW', '中国台湾' => 'TW', 'taiwan' => 'TW', 'tw' => 'TW',
|
||||
'新加坡' => 'SG', 'singapore' => 'SG', 'sg' => 'SG',
|
||||
'英国' => 'GB', 'united kingdom' => 'GB', 'uk' => 'GB', 'gb' => 'GB',
|
||||
'德国' => 'DE', 'germany' => 'DE', 'de' => 'DE',
|
||||
'法国' => 'FR', 'france' => 'FR', 'fr' => 'FR',
|
||||
'俄罗斯' => 'RU', 'russia' => 'RU', 'ru' => 'RU',
|
||||
'澳大利亚' => 'AU', 'australia' => 'AU', 'au' => 'AU',
|
||||
'加拿大' => 'CA', 'canada' => 'CA', 'ca' => 'CA',
|
||||
'印度' => 'IN', 'india' => 'IN', 'in' => 'IN',
|
||||
'越南' => 'VN', 'vietnam' => 'VN', 'vn' => 'VN',
|
||||
'泰国' => 'TH', 'thailand' => 'TH', 'th' => 'TH',
|
||||
'马来西亚' => 'MY', 'malaysia' => 'MY', 'my' => 'MY',
|
||||
'菲律宾' => 'PH', 'philippines' => 'PH', 'ph' => 'PH',
|
||||
'印度尼西亚' => 'ID', 'indonesia' => 'ID', 'id' => 'ID',
|
||||
];
|
||||
$key = function_exists('mb_strtolower') ? mb_strtolower($raw, 'UTF-8') : strtolower($raw);
|
||||
if (isset($map[$raw])) {
|
||||
return $map[$raw];
|
||||
}
|
||||
if (isset($map[$key])) {
|
||||
return $map[$key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,7 +205,16 @@ trait ModerationPart2 {
|
||||
// ============================================================
|
||||
|
||||
private function get_client_ip(): string {
|
||||
$keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'];
|
||||
if (function_exists('aksrt_get_client_ip')) {
|
||||
return (string) aksrt_get_client_ip();
|
||||
}
|
||||
$keys = [
|
||||
'HTTP_CF_CONNECTING_IP',
|
||||
'HTTP_TRUE_CLIENT_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_REAL_IP',
|
||||
'HTTP_CLIENT_IP',
|
||||
];
|
||||
foreach ($keys as $key) {
|
||||
if (!empty($_SERVER[$key])) {
|
||||
$parts = explode(',', (string) $_SERVER[$key]);
|
||||
|
||||
@@ -292,7 +292,10 @@ class CommentActions {
|
||||
'comment_content' => $content,
|
||||
'comment_parent' => $parent,
|
||||
'comment_type' => '',
|
||||
'comment_author_IP' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash((string) $_SERVER['REMOTE_ADDR'])) : '',
|
||||
// Prefer real client IP behind CDN/proxy (REMOTE_ADDR alone is often edge IP).
|
||||
'comment_author_IP' => function_exists('aksrt_get_client_ip')
|
||||
? (string) aksrt_get_client_ip()
|
||||
: (isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash((string) $_SERVER['REMOTE_ADDR'])) : ''),
|
||||
'comment_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash((string) $_SERVER['HTTP_USER_AGENT'])) : '',
|
||||
'comment_date' => current_time('mysql'),
|
||||
'comment_date_gmt' => current_time('mysql', true),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -53,19 +53,20 @@ class ExtensionRuntime {
|
||||
$provider = 'null';
|
||||
}
|
||||
|
||||
$display_raw = $settings['ip_geo_display'] ?? ['comment', 'user_home', 'user_center', 'post'];
|
||||
$display = is_array($display_raw) ? $display_raw : [];
|
||||
$allowed_display = ['comment', 'user_home', 'user_center', 'post'];
|
||||
$display_norm = [];
|
||||
foreach ($display as $item) {
|
||||
$key = sanitize_key((string) $item);
|
||||
if (in_array($key, $allowed_display, true) && !in_array($key, $display_norm, true)) {
|
||||
$display_norm[] = $key;
|
||||
}
|
||||
}
|
||||
// Empty / missing → full four surfaces (canonical default).
|
||||
if ($display_norm === []) {
|
||||
// Only fill defaults when the key was never saved — empty array means "none".
|
||||
if (!array_key_exists('ip_geo_display', $settings)) {
|
||||
$display_norm = $allowed_display;
|
||||
} else {
|
||||
$display_raw = $settings['ip_geo_display'];
|
||||
$display = is_array($display_raw) ? $display_raw : [];
|
||||
$display_norm = [];
|
||||
foreach ($display as $item) {
|
||||
$key = sanitize_key((string) $item);
|
||||
if (in_array($key, $allowed_display, true) && !in_array($key, $display_norm, true)) {
|
||||
$display_norm[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$format = sanitize_key((string) ($settings['ip_geo_format'] ?? 'province_city'));
|
||||
|
||||
Reference in New Issue
Block a user