diff --git a/assets/admin/src/components/form/AksFormRenderer.vue b/assets/admin/src/components/form/AksFormRenderer.vue index 30bd9c18..5a1b7bf4 100644 --- a/assets/admin/src/components/form/AksFormRenderer.vue +++ b/assets/admin/src/components/form/AksFormRenderer.vue @@ -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) diff --git a/assets/admin/src/schema/sections/extensions/ip.js b/assets/admin/src/schema/sections/extensions/ip.js index 2436891c..8fbf165c 100644 --- a/assets/admin/src/schema/sections/extensions/ip.js +++ b/assets/admin/src/schema/sections/extensions/ip.js @@ -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: '若控制台启用了「数字签名」,必填安全密钥;未启用可留空。' }, ], }, { diff --git a/inc/helpers/aksrt-ip-geo.php b/inc/helpers/aksrt-ip-geo.php index 807b63c4..862f71e8 100644 --- a/inc/helpers/aksrt-ip-geo.php +++ b/inc/helpers/aksrt-ip-geo.php @@ -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; diff --git a/inc/helpers/comments/geo.php b/inc/helpers/comments/geo.php index 44841dc2..10574171 100644 --- a/inc/helpers/comments/geo.php +++ b/inc/helpers/comments/geo.php @@ -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; } diff --git a/inc/modules/captcha.php b/inc/modules/captcha.php index 78421136..8d20add9 100644 --- a/inc/modules/captcha.php +++ b/inc/modules/captcha.php @@ -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 签名 // ============================================================ diff --git a/src/AKSRT/Admin/ExtensionController.php b/src/AKSRT/Admin/ExtensionController.php index 7803eb4e..dd77b3fe 100644 --- a/src/AKSRT/Admin/ExtensionController.php +++ b/src/AKSRT/Admin/ExtensionController.php @@ -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) { diff --git a/src/AKSRT/Admin/Schema/SchemaExtensionsIP.php b/src/AKSRT/Admin/Schema/SchemaExtensionsIP.php index 6e72e6ae..c327141a 100644 --- a/src/AKSRT/Admin/Schema/SchemaExtensionsIP.php +++ b/src/AKSRT/Admin/Schema/SchemaExtensionsIP.php @@ -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' => '启用数字签名时必填'], ]], diff --git a/src/AKSRT/Auth/ModerationPart2.php b/src/AKSRT/Auth/ModerationPart2.php index ff4cf6eb..a47e5f78 100644 --- a/src/AKSRT/Auth/ModerationPart2.php +++ b/src/AKSRT/Auth/ModerationPart2.php @@ -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]); diff --git a/src/AKSRT/Core/CommentActions.php b/src/AKSRT/Core/CommentActions.php index 649bed2b..f544e48b 100644 --- a/src/AKSRT/Core/CommentActions.php +++ b/src/AKSRT/Core/CommentActions.php @@ -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), diff --git a/src/AKSRT/Extensions/APIAudit.php b/src/AKSRT/Extensions/APIAudit.php index 1070c9ba..992c2a44 100644 --- a/src/AKSRT/Extensions/APIAudit.php +++ b/src/AKSRT/Extensions/APIAudit.php @@ -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); } diff --git a/src/AKSRT/Extensions/IPGeo.php b/src/AKSRT/Extensions/IPGeo.php index d1c5578e..93c7f02b 100644 --- a/src/AKSRT/Extensions/IPGeo.php +++ b/src/AKSRT/Extensions/IPGeo.php @@ -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', diff --git a/src/AKSRT/Settings/ExtensionRuntime.php b/src/AKSRT/Settings/ExtensionRuntime.php index 4dfc34c2..2a403e0d 100644 --- a/src/AKSRT/Settings/ExtensionRuntime.php +++ b/src/AKSRT/Settings/ExtensionRuntime.php @@ -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'));