Files
wordpress-theme-aksrt/src/Admin/ExtensionController.php
T

397 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
class ExtensionController extends ControllerBase {
private static $instance = null;
private $admin;
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
public function set_admin(Admin $admin): void {
$this->admin = $admin;
}
public function register_routes(): void {
// Extensions overview
register_rest_route(self::API_NAMESPACE, '/extensions/overview', [
'methods' => 'GET',
'callback' => [$this, 'get_overview'],
'permission_callback' => [$this, 'check_permission'],
]);
// Extensions service status summary (aggregated)
register_rest_route(self::API_NAMESPACE, '/settings/extensions', [
'methods' => 'GET',
'callback' => [$this, 'get_extensions_status'],
'permission_callback' => [$this, 'check_permission'],
]);
// IP geo test tool
register_rest_route(self::API_NAMESPACE, '/extensions/ip-test', [
'methods' => 'POST',
'callback' => [$this, 'test_ip_geo'],
'permission_callback' => [$this, 'check_permission'],
]);
// Content audit test tool
register_rest_route(self::API_NAMESPACE, '/extensions/audit-test', [
'methods' => 'POST',
'callback' => [$this, 'test_content_audit'],
'permission_callback' => [$this, 'check_permission'],
]);
}
/**
* GET /extensions/overview — real extension status aggregation.
*/
public function get_overview(): \WP_REST_Response {
$services = $this->collect_service_statuses();
$installed = 0;
$available = count($services);
$unconfigured = 0;
foreach ($services as $svc) {
if (!empty($svc['enabled'])) $installed++;
if (!empty($svc['enabled']) && !empty($svc['configured']) && empty($svc['verified'])) {
$unconfigured++;
}
}
// Build message
if ($installed === 0) {
$message = sprintf(__('已注册 %d 项扩展,均未启用。', 'aksrt'), $available);
} elseif ($unconfigured > 0) {
$message = sprintf(__('已启用 %d/%d 项扩展,%d 项待补全配置。', 'aksrt'), $installed, $available, $unconfigured);
} else {
$message = sprintf(__('已启用 %d/%d 项扩展,全部就绪。', 'aksrt'), $installed, $available);
}
return $this->success([
'data' => [
'installed' => $installed,
'available' => $available,
'message' => $message,
'services' => $services,
],
]);
}
public function get_docs_stats(): \WP_REST_Response {
return $this->success([
'data' => [
'total' => 0,
'nav_count' => 0,
],
]);
}
/**
* GET /docs/search — search document pages by keyword.
*/
public function search_docs(\WP_REST_Request $request): \WP_REST_Response {
return $this->success([
'data' => [
'results' => [],
'total' => 0,
],
]);
}
/**
* GET /settings/extensions — aggregated extension service status.
*/
public function get_extensions_status(): \WP_REST_Response {
$services = $this->collect_service_statuses();
$flat = [];
foreach ($services as $key => $svc) {
$flat["{$key}_enabled"] = !empty($svc['enabled']);
$flat["{$key}_configured"] = !empty($svc['configured']);
$flat["{$key}_available"] = !empty($svc['available']);
$flat["{$key}_verified"] = !empty($svc['verified']);
$flat["{$key}_provider"] = (string) ($svc['provider'] ?? '');
}
return $this->success([
'data' => $flat,
'services' => $services,
]);
}
/**
* POST /extensions/ip-test — test current IP provider with a given IP.
*
* Admin tests force a live lookup (skip enable-gate + cache) so operators can
* verify credentials before flipping the runtime switch.
*/
public function test_ip_geo(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$ip = trim((string) ($params['ip'] ?? ''));
if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) {
return $this->error(__('请输入有效的 IP 地址', 'aksrt'), 422);
}
if (!class_exists('\AKSRT\Extensions\IPGeo')) {
return $this->error(__('IP 归属地服务不可用', 'aksrt'), 500);
}
$geo = \AKSRT\Extensions\IPGeo::get_instance();
$status = $geo->get_status();
$result = $geo->lookup($ip, true);
$reason = (string) ($result['reason'] ?? '');
$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) {
case 'private_ip':
$message = __('该地址为私有/保留 IP(如 127.0.0.1 / 内网),无法查询。请用公网 IP 测试,例如 114.114.114.114。', 'aksrt');
break;
case 'invalid_ip':
$message = __('IP 格式无效。', 'aksrt');
break;
case 'missing_key':
$message = __('当前服务商 AppKey 未填写,请在「扩展 → IP归属地」补全凭据。', 'aksrt');
break;
case 'provider_error':
// e.g. USERKEY_PLAT_NOMATCH / INVALID_USER_KEY from Amap
if (stripos($detail, 'USERKEY_PLAT_NOMATCH') !== false) {
$message = __('高德返回 USERKEY_PLAT_NOMATCH:当前 Key 平台类型不匹配。请在高德开放平台新建「Web服务」类型 Key,并开通 IP 定位,不要用 Web端(JS)/Android/iOS Key。', 'aksrt');
} elseif (stripos($detail, 'INVALID_USER_KEY') !== false) {
$message = __('高德返回 INVALID_USER_KEY:AppKey 无效或未启用,请核对 Key 是否复制完整、是否已启用。', 'aksrt');
} else {
$message = $detail !== ''
? sprintf(
/* translators: %s: provider error code/message */
__('服务商拒绝请求:%s。请检查 Key 权限(IP定位)与配额。', 'aksrt'),
$detail
)
: __('服务商返回错误,请检查 Key 权限与配额。', 'aksrt');
}
break;
case 'http_error':
$message = $detail !== ''
? sprintf(
/* translators: %s: transport error */
__('无法访问归属地接口:%s。请检查服务器出站网络/防火墙。', 'aksrt'),
$detail
)
: __('无法访问归属地接口,请检查服务器出站网络。', 'aksrt');
break;
case 'provider_empty':
if (!empty($status['missing_fields'])) {
$message = sprintf(
/* translators: %s: missing config field keys */
__('当前服务商凭据不完整(缺少:%s)。', 'aksrt'),
implode(', ', (array) $status['missing_fields'])
);
} else {
$message = __('服务商未返回有效归属地,请换公网 IP 或检查 Key 权限。', 'aksrt');
}
break;
case 'disabled':
$message = __('IP 归属地功能未启用。', 'aksrt');
break;
default:
$message = $detail !== '' ? $detail : __('未查询到归属地信息。', 'aksrt');
}
}
return $this->success([
'data' => [
'ip' => $ip,
'ok' => $ok,
'message' => $message,
'status' => $status,
'result' => $result,
'display' => $ok ? $geo->format($result) : '',
],
]);
}
/**
* POST /extensions/audit-test — test text content against current audit provider.
*
* Admin tests force a live provider call (skip enable gates + failure allow policy)
* so operators can verify credentials before flipping runtime switches.
*/
public function test_content_audit(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$text = trim((string) ($params['text'] ?? ''));
if ($text === '') {
return $this->error(__('请输入要测试的文本内容', 'aksrt'), 422);
}
if (!class_exists('\AKSRT\Extensions\ContentAudit')) {
return $this->error(__('内容审核服务不可用', 'aksrt'), 500);
}
$audit = \AKSRT\Extensions\ContentAudit::get_instance();
$status = $audit->get_status();
$result = $audit->audit_text($text, true);
$reason = (string) ($result['reason'] ?? '');
$message = '';
$ok = !empty($result['passed']);
// config/api 错误时 passed=false 且 reason 是机器码,需要可读提示
$error_reasons = ['config_missing', 'api_error', 'file_error', 'disabled'];
$is_error = in_array($reason, $error_reasons, true)
|| (str_starts_with($reason, 'config_') && $reason !== '');
if ($is_error || (!$ok && $reason !== '')) {
switch ($reason) {
case 'config_missing':
if (!empty($status['missing_fields'])) {
$message = sprintf(
/* translators: %s: missing config field keys */
__('当前审核服务商凭据不完整(缺少:%s),请先在「扩展 → 内容审核」补全配置。', 'aksrt'),
implode(', ', (array) $status['missing_fields'])
);
} else {
$message = __('审核服务商凭据缺失,请先补全 API Key / Secret。', 'aksrt');
}
$ok = false;
break;
case 'api_error':
$message = __('调用审核接口失败,请检查凭据、网络出口或服务商状态。', 'aksrt');
$ok = false;
break;
case 'disabled':
$message = __('内容审核功能未启用。', 'aksrt');
$ok = false;
break;
case 'non_compliant':
$message = __('审核结论:不合规(已拦截)。', 'aksrt');
break;
case 'suspected':
$message = __('审核结论:疑似违规。', 'aksrt');
break;
case 'review_failed':
$message = __('审核结论:服务商审核失败。', 'aksrt');
break;
default:
if (!$ok) {
$message = $reason !== ''
? sprintf(/* translators: %s: provider reason */ __('审核未通过:%s', 'aksrt'), $reason)
: __('审核未通过。', 'aksrt');
}
}
} elseif ($ok) {
if ($reason === 'passthrough' || $reason === '') {
$message = __('审核通过。', 'aksrt');
} else {
$message = sprintf(/* translators: %s: provider reason */ __('审核通过(%s)。', 'aksrt'), $reason);
}
}
return $this->success([
'data' => [
'text' => $text,
'ok' => $ok,
'message' => $message,
'status' => $status,
'result' => $result,
],
]);
}
/**
* 收集所有扩展服务的运行时状态
*/
private function collect_service_statuses(): array {
$services = [];
// SMS
if (class_exists('\AKSRT\Sms\SMS')) {
$services['sms'] = \AKSRT\Sms\SMS::get_instance()->get_provider_status();
} else {
$services['sms'] = ['enabled' => false, 'provider' => 'none', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
}
// WeChat
if (class_exists('\AKSRT\Payment\WeChatNotify')) {
$services['wechat'] = \AKSRT\Payment\WeChatNotify::get_instance()->get_status();
} else {
$services['wechat'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
}
// Captcha — 唯一真相源 aksrt_captcha_config
$captcha = function_exists('aksrt_get_captcha_status_summary')
? aksrt_get_captcha_status_summary()
: ['enabled' => false, 'provider' => '', 'configured' => false, 'scenes' => []];
$services['captcha'] = [
'enabled' => !empty($captcha['enabled']) && !empty($captcha['configured']),
'provider' => (string) ($captcha['provider'] ?? ''),
'configured' => !empty($captcha['configured']),
'available' => !empty($captcha['configured']),
'verified' => false,
'transport' => 'http_api',
'missing_fields' => [],
'scenes' => $captcha['scenes'] ?? [],
];
// IP Geo
if (class_exists('\AKSRT\Extensions\IPGeo')) {
$services['ip_geo'] = \AKSRT\Extensions\IPGeo::get_instance()->get_status();
} else {
$services['ip_geo'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
}
// Content Audit
if (class_exists('\AKSRT\Extensions\ContentAudit')) {
$services['audit'] = \AKSRT\Extensions\ContentAudit::get_instance()->get_status();
} else {
$services['audit'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
}
// Search Push
if (class_exists('\AKSRT\Extensions\SearchPush')) {
$services['push'] = \AKSRT\Extensions\SearchPush::get_instance()->get_status();
} else {
$services['push'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
}
// Docs Mode (compat path is retired from rebuilt mainline)
$services['docs_mode'] = [
'enabled' => false,
'provider' => 'builtin',
'configured' => false,
'available' => false,
'verified' => true,
'transport' => 'builtin',
'missing_fields' => [],
];
return $services;
}
}