Files
wordpress-theme-aksrt/inc/helpers/aksrt-ip-geo.php
T
kate522 59f5d5d98a 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.
2026-07-27 15:39:16 +08:00

337 lines
10 KiB
PHP

<?php
/**
* IP 归属地 — storage, format, and display (no dual-read / no legacy keys).
*
* Gates:
* master: ip_geo_enabled
* surface: ip_geo_display ∈ {comment, user_home, user_center, post}
* users: user_city_s (+ user_city_type)
* comments: comment_city_s (+ comment_city_type) via comments/geo.php
*
* Meta:
* comment → aksrt_comment_addr {country,province,city}
* user → aksrt_user_addr, aksrt_register_ip
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_get_ip_geo_config')) :
/**
* @return array<string,mixed>
*/
function aksrt_get_ip_geo_config(): array {
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_ip_geo_settings')) {
return \AKSRT\Settings\Settings::get_ip_geo_settings();
}
return [
'ip_geo_enabled' => false,
'ip_geo_display' => ['comment', 'user_home', 'user_center', 'post'],
'ip_geo_format' => 'province_city',
'ip_geo_fallback_text' => '',
];
}
endif;
if (!function_exists('aksrt_ip_geo_master_enabled')) :
function aksrt_ip_geo_master_enabled(): bool {
return !empty(aksrt_get_ip_geo_config()['ip_geo_enabled']);
}
endif;
if (!function_exists('aksrt_ip_geo_display_list')) :
/**
* @return list<string>
*/
function aksrt_ip_geo_display_list(): array {
$c = aksrt_get_ip_geo_config();
$display = $c['ip_geo_display'] ?? [];
if (!is_array($display)) {
$display = [];
}
$allowed = ['comment', 'user_home', 'user_center', 'post'];
$out = [];
foreach ($display as $item) {
$key = sanitize_key((string) $item);
if (in_array($key, $allowed, true) && !in_array($key, $out, true)) {
$out[] = $key;
}
}
return $out;
}
endif;
if (!function_exists('aksrt_ip_geo_display_enabled')) :
/**
* @param string $location comment|user_home|user_center|post
*/
function aksrt_ip_geo_display_enabled(string $location): bool {
if (!aksrt_ip_geo_master_enabled()) {
return false;
}
$location = sanitize_key($location);
if (!in_array($location, aksrt_ip_geo_display_list(), true)) {
return false;
}
if (in_array($location, ['user_home', 'user_center'], true) && !aksrt_user_city_enabled()) {
return false;
}
return true;
}
endif;
if (!function_exists('aksrt_ip_geo_format_key')) :
function aksrt_ip_geo_format_key(): string {
$format = sanitize_key((string) (aksrt_get_ip_geo_config()['ip_geo_format'] ?? 'province_city'));
return in_array($format, ['province_city', 'province', 'country_province'], true)
? $format
: 'province_city';
}
endif;
if (!function_exists('aksrt_user_city_enabled')) :
/**
* Users feature: show city on user home / user center.
* Default true when key absent (zibll parity for new installs).
*/
function aksrt_user_city_enabled(): bool {
if (!class_exists('\AKSRT\Settings\Settings')) {
return true;
}
$all = \AKSRT\Settings\Settings::get_all();
if (!array_key_exists('user_city_s', $all)) {
return true;
}
return !empty($all['user_city_s']);
}
endif;
if (!function_exists('aksrt_user_city_type')) :
function aksrt_user_city_type(): string {
if (!class_exists('\AKSRT\Settings\Settings')) {
return aksrt_ip_geo_format_key();
}
$all = \AKSRT\Settings\Settings::get_all();
$type = sanitize_key((string) ($all['user_city_type'] ?? ''));
if (in_array($type, ['province_city', 'province', 'country_province'], true)) {
return $type;
}
return aksrt_ip_geo_format_key();
}
endif;
if (!function_exists('aksrt_normalize_geo_addr')) :
/**
* @param mixed $raw Meta value.
* @return array{country:string,province:string,city:string}
*/
function aksrt_normalize_geo_addr($raw): array {
if (!is_array($raw)) {
return ['country' => '', 'province' => '', 'city' => ''];
}
return [
'country' => trim((string) ($raw['country'] ?? '')),
'province' => trim((string) ($raw['province'] ?? '')),
'city' => trim((string) ($raw['city'] ?? '')),
];
}
endif;
if (!function_exists('aksrt_format_geo_label')) :
/**
* @param array<string,string> $addr Addr.
* @param string $type Format key.
*/
function aksrt_format_geo_label(array $addr, string $type = 'province_city'): string {
$addr = aksrt_normalize_geo_addr($addr);
if (class_exists('\AKSRT\Extensions\IPGeo')) {
$label = \AKSRT\Extensions\IPGeo::get_instance()->format($addr, $type);
if (is_string($label) && $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':
if ($province !== '') {
return $province;
}
return $city !== '' ? $city : $country;
case 'country_province':
$joined = trim($country . ' ' . $province);
if ($joined !== '') {
return $joined;
}
return $city;
case 'province_city':
default:
$parts = array_filter([$province, $city]);
if (!empty($parts)) {
return implode(' ', $parts);
}
return $country;
}
}
endif;
if (!function_exists('aksrt_get_geo_badge_html')) :
/**
* @param array<string,string>|mixed $addr Addr meta.
* @param string $type Format.
* @param string $class CSS classes.
* @param string $title Title attr.
*/
function aksrt_get_geo_badge_html($addr, string $type = 'province_city', string $class = 'badg badg-sm', string $title = ''): string {
$label = aksrt_format_geo_label(is_array($addr) ? $addr : [], $type);
if ($label === '') {
$fallback = (string) (aksrt_get_ip_geo_config()['ip_geo_fallback_text'] ?? '');
if ($fallback === '') {
return '';
}
$label = $fallback;
}
if ($title === '') {
$title = __('IP 属地', 'aksrt');
}
$class = trim($class . ' aksrt-ip-geo-badge');
return '<span class="' . esc_attr($class) . '" title="' . esc_attr($title) . '">' . esc_html($label) . '</span>';
}
endif;
if (!function_exists('aksrt_lookup_geo_for_ip')) :
/**
* @return array{country:string,province:string,city:string}
*/
function aksrt_lookup_geo_for_ip(string $ip): array {
$ip = trim($ip);
if ($ip === '' || !class_exists('\AKSRT\Extensions\IPGeo')) {
return ['country' => '', 'province' => '', 'city' => ''];
}
$geo = \AKSRT\Extensions\IPGeo::get_instance()->lookup($ip);
return aksrt_normalize_geo_addr(is_array($geo) ? $geo : []);
}
endif;
if (!function_exists('aksrt_store_user_addr')) :
/**
* Resolve IP and write aksrt_user_addr (login / register / own-page refresh).
*/
function aksrt_store_user_addr(int $user_id, ?string $ip = null): void {
if ($user_id <= 0 || !aksrt_ip_geo_master_enabled()) {
return;
}
$need = aksrt_user_city_enabled()
|| in_array('post', aksrt_ip_geo_display_list(), true)
|| in_array('user_home', aksrt_ip_geo_display_list(), true)
|| in_array('user_center', aksrt_ip_geo_display_list(), true);
if (!$need) {
return;
}
if ($ip === null || $ip === '') {
$ip = function_exists('aksrt_get_client_ip') ? (string) aksrt_get_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? '');
}
$ip = trim($ip);
if ($ip === '') {
return;
}
$addr = aksrt_lookup_geo_for_ip($ip);
if ($addr['country'] === '' && $addr['province'] === '' && $addr['city'] === '') {
return;
}
update_user_meta($user_id, 'aksrt_user_addr', $addr);
}
endif;
if (!function_exists('aksrt_get_user_addr')) :
/**
* Read aksrt_user_addr; if empty and viewing self, resolve current client IP once.
*
* @return array{country:string,province:string,city:string}
*/
function aksrt_get_user_addr(int $user_id): array {
if ($user_id <= 0) {
return ['country' => '', 'province' => '', 'city' => ''];
}
$addr = aksrt_normalize_geo_addr(get_user_meta($user_id, 'aksrt_user_addr', true));
if ($addr['country'] !== '' || $addr['province'] !== '' || $addr['city'] !== '') {
return $addr;
}
// Self view: write-on-read from current IP (same outcome as next login).
if ((int) get_current_user_id() === $user_id && aksrt_ip_geo_master_enabled()) {
aksrt_store_user_addr($user_id);
return aksrt_normalize_geo_addr(get_user_meta($user_id, 'aksrt_user_addr', true));
}
return $addr;
}
endif;
if (!function_exists('aksrt_get_user_city_badge_html')) :
/**
* @param int $user_id User.
* @param string $location user_home|user_center
* @param string $class CSS.
*/
function aksrt_get_user_city_badge_html(int $user_id, string $location = 'user_home', string $class = 'badg'): string {
if ($user_id <= 0 || !aksrt_ip_geo_display_enabled($location)) {
return '';
}
return aksrt_get_geo_badge_html(aksrt_get_user_addr($user_id), aksrt_user_city_type(), $class);
}
endif;
if (!function_exists('aksrt_get_post_author_city_badge_html')) :
/**
* Article author IP 属地 (author aksrt_user_addr).
*/
function aksrt_get_post_author_city_badge_html(int $author_id = 0, string $class = 'badg badg-sm'): string {
if (!aksrt_ip_geo_display_enabled('post')) {
return '';
}
if ($author_id <= 0) {
$author_id = (int) get_the_author_meta('ID');
}
if ($author_id <= 0) {
return '';
}
$type = aksrt_user_city_enabled() ? aksrt_user_city_type() : aksrt_ip_geo_format_key();
return aksrt_get_geo_badge_html(aksrt_get_user_addr($author_id), $type, $class);
}
endif;
add_action('wp_login', static function ($user_login, $user = null): void {
$user_id = 0;
if ($user instanceof WP_User) {
$user_id = (int) $user->ID;
} elseif (is_string($user_login) && $user_login !== '') {
$u = get_user_by('login', $user_login);
$user_id = $u instanceof WP_User ? (int) $u->ID : 0;
}
if ($user_id > 0) {
aksrt_store_user_addr($user_id);
}
}, 20, 2);
add_action('user_register', static function ($user_id): void {
$user_id = (int) $user_id;
if ($user_id <= 0) {
return;
}
$ip = function_exists('aksrt_get_client_ip') ? (string) aksrt_get_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? '');
$ip = trim($ip);
if ($ip !== '') {
update_user_meta($user_id, 'aksrt_register_ip', $ip);
}
aksrt_store_user_addr($user_id, $ip !== '' ? $ip : null);
}, 20, 1);