Files
wordpress-theme-aksrt/src/AKSRT/Extensions/IPGeo.php
T
kate522 aaf458af1c refactor(autoload): migrate theme classes to Composer PSR-4
Move 288 types from global AKSRT_* + inc/classes manual requires into
namespace AKSRT\{Domain} under src/AKSRT/, wire composer.json psr-4,
and slim bootstrap to helpers/modules/hooks only. Split multi-class
widget/gateway files, update admin contract audit paths, and document
the layout in ADR/README.
2026-07-17 13:29:22 +08:00

230 lines
8.0 KiB
PHP

<?php
/**
* @package AKSRT
*/
namespace AKSRT\Extensions;
defined('ABSPATH') || exit;
class IPGeo {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
/**
* 查询 IP 归属地
*
* @param string $ip IPv4 or IPv6
* @param bool $force 管理端测试用:跳过「启用」开关与缓存,仍拒绝私有 IP
* @return array{country: string, province: string, city: string, raw: array, reason?: string}
*/
public function lookup(string $ip, bool $force = false): array {
$config = $this->get_config();
if (!$force && empty($config['ip_geo_enabled'])) {
return $this->empty_result('disabled');
}
// 本地/私有 IP 跳过查询
if ($this->is_private_ip($ip)) {
return $this->empty_result('private_ip');
}
$cache_key = 'aksrt_ip_geo_' . md5($ip);
$ttl = max(3600, (int) ($config['ip_geo_cache_ttl'] ?? 86400));
if (!$force) {
$cached = get_transient($cache_key);
if ($cached !== false && is_array($cached)) {
return $cached;
}
}
$provider = sanitize_key((string) ($config['ip_addr_sdk'] ?? ($config['ip_geo_provider'] ?? 'null')));
$result = $this->query_provider($provider, $ip, $config);
if (!empty($result['province']) || !empty($result['city']) || !empty($result['country'])) {
if (!$force) {
set_transient($cache_key, $result, $ttl);
}
return $result;
}
if (empty($result['reason'])) {
$result['reason'] = 'provider_empty';
}
return $result;
}
/**
* 获取服务运行时状态
*/
public function get_status(): array {
$config = $this->get_config();
$provider = sanitize_key((string) ($config['ip_addr_sdk'] ?? ($config['ip_geo_provider'] ?? 'null')));
$enabled = !empty($config['ip_geo_enabled']);
$missing = [];
if ($enabled && $provider === 'qq') {
if (empty($config['ip_addr_sdk_qq_appkey'])) $missing[] = 'ip_addr_sdk_qq_appkey';
if (empty($config['ip_addr_sdk_qq_secretkey'])) $missing[] = 'ip_addr_sdk_qq_secretkey';
}
if ($enabled && $provider === 'amap') {
if (empty($config['ip_addr_sdk_amap_appkey'])) $missing[] = 'ip_addr_sdk_amap_appkey';
if (empty($config['ip_addr_sdk_amap_secretkey'])) $missing[] = 'ip_addr_sdk_amap_secretkey';
}
return [
'enabled' => $enabled,
'provider' => $provider,
'configured' => $enabled && empty($missing),
'available' => $enabled,
'verified' => !empty($config['ip_geo_verified']),
'transport' => 'http_api',
'missing_fields' => $missing,
'cache_ttl' => (int) ($config['ip_geo_cache_ttl'] ?? 86400),
'display' => (array) ($config['ip_geo_display'] ?? ['comment']),
'format' => (string) ($config['ip_geo_format'] ?? 'province_city'),
];
}
/**
* 格式化归属地显示文本
*/
public function format(array $geo, string $format = ''): string {
if ($format === '') {
$config = $this->get_config();
$format = (string) ($config['ip_geo_format'] ?? 'province_city');
}
switch ($format) {
case 'province':
return $geo['province'] ?? '';
case 'country_province':
return trim(($geo['country'] ?? '') . ' ' . ($geo['province'] ?? ''));
case 'province_city':
default:
$parts = array_filter([$geo['province'] ?? '', $geo['city'] ?? '']);
return implode(' ', $parts) ?: ($geo['country'] ?? '');
}
}
private function get_config(): array {
if (class_exists('\AKSRT\Settings\Settings')) {
return \AKSRT\Settings\Settings::get_ip_geo_settings();
}
return ['ip_geo_enabled' => false];
}
private function query_provider(string $provider, string $ip, array $config): array {
switch ($provider) {
case 'qq':
return $this->query_qq($ip, $config);
case 'amap':
return $this->query_amap($ip, $config);
case 'polling':
foreach (['qq', 'amap', 'null'] as $fallback) {
$result = $this->query_provider($fallback, $ip, $config);
if (!empty($result['province']) || !empty($result['city']) || !empty($result['country'])) {
return $result;
}
}
return $this->empty_result();
case 'null':
default:
return $this->query_ip_api($ip);
}
}
private function query_qq(string $ip, array $config): array {
$appkey = trim((string) ($config['ip_addr_sdk_qq_appkey'] ?? ''));
if ($appkey === '') {
return $this->empty_result();
}
$url = add_query_arg([
'ip' => $ip,
'key' => $appkey,
], 'https://apis.map.qq.com/ws/location/v1/ip');
$response = wp_remote_get($url, [
'timeout' => 8,
'headers' => ['Accept' => 'application/json'],
]);
if (is_wp_error($response)) return $this->empty_result();
$data = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($data) || (int) ($data['status'] ?? 1) !== 0) return $this->empty_result();
$info = $data['result']['ad_info'] ?? [];
return [
'country' => '中国',
'province' => (string) ($info['province'] ?? ''),
'city' => (string) ($info['city'] ?? ''),
'raw' => $data,
];
}
private function query_amap(string $ip, array $config): array {
$appkey = trim((string) ($config['ip_addr_sdk_amap_appkey'] ?? ''));
if ($appkey === '') {
return $this->empty_result();
}
$url = add_query_arg([
'ip' => $ip,
'key' => $appkey,
], 'https://restapi.amap.com/v3/ip');
$response = wp_remote_get($url, [
'timeout' => 8,
'headers' => ['Accept' => 'application/json'],
]);
if (is_wp_error($response)) return $this->empty_result();
$data = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($data) || (string) ($data['status'] ?? '0') !== '1') return $this->empty_result();
return [
'country' => '中国',
'province' => (string) ($data['province'] ?? ''),
'city' => (string) ($data['city'] ?? ''),
'raw' => $data,
];
}
private function query_ip_api(string $ip): array {
$response = wp_remote_get("http://ip-api.com/json/{$ip}?lang=zh-CN", [
'timeout' => 5,
]);
if (is_wp_error($response)) return $this->empty_result();
$data = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($data) || ($data['status'] ?? '') !== 'success') return $this->empty_result();
return [
'country' => (string) ($data['country'] ?? ''),
'province' => (string) ($data['regionName'] ?? ''),
'city' => (string) ($data['city'] ?? ''),
'raw' => $data,
];
}
private function is_private_ip(string $ip): bool {
return !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
}
private function empty_result(string $reason = ''): array {
$out = ['country' => '', 'province' => '', 'city' => '', 'raw' => []];
if ($reason !== '') {
$out['reason'] = $reason;
}
return $out;
}
}