397 lines
16 KiB
PHP
397 lines
16 KiB
PHP
<?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;
|
||
}
|
||
}
|