Files
wordpress-theme-aksrt/src/Extensions/ContentAudit.php
T
kate522 8c5b1555c9 refactor: flatten src/AKSRT → src (drop theme-identifier dir)
Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
2026-08-02 22:06:06 +08:00

366 lines
14 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\Extensions;
defined('ABSPATH') || exit;
class ContentAudit {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 审核文本内容
*
* @param string $text 待审文本
* @param bool $force 管理端测试用:跳过启用开关与失败放行策略,直接打服务商
* @return array{passed: bool, reason: string, labels: array, raw: array}
*/
public function audit_text(string $text, bool $force = false): array {
$config = $this->get_config();
if (
!$force
&& (empty($config['content_audit_enabled']) || empty($config['content_audit_text_enabled']))
) {
return $this->ok('disabled');
}
$provider = sanitize_key((string) ($config['content_audit_provider'] ?? 'baidu'));
$result = $this->call_text_api($provider, $text, $config);
// 管理端强制测试不写生产日志,避免污染审计流水
if (!$force && !empty($config['content_audit_log_enabled'])) {
$this->log_audit('text', $text, $result);
}
// 管理端强制测试保留服务商原始结论,不套用 on_failure=allow
if (
!$force
&& !$result['passed']
&& ($config['content_audit_on_failure'] ?? 'allow') === 'allow'
) {
$result['passed'] = true;
$result['reason'] .= ' (failure_policy: allow)';
}
return $result;
}
/**
* 审核图片
*
* @param string $image_path 本地文件路径或 URL
* @return array{passed: bool, reason: string, labels: array, raw: array}
*/
public function audit_image(string $image_path): array {
$config = $this->get_config();
if (empty($config['content_audit_enabled']) || empty($config['content_audit_image_enabled'])) {
return $this->ok('disabled');
}
$provider = sanitize_key((string) ($config['content_audit_provider'] ?? 'baidu'));
$result = $this->call_image_api($provider, $image_path, $config);
if (!empty($config['content_audit_log_enabled'])) {
$this->log_audit('image', $image_path, $result);
}
if (!$result['passed'] && ($config['content_audit_on_failure'] ?? 'allow') === 'allow') {
$result['passed'] = true;
$result['reason'] .= ' (failure_policy: allow)';
}
return $result;
}
/**
* 获取服务运行时状态
*/
public function get_status(): array {
$config = $this->get_config();
$enabled = !empty($config['content_audit_enabled']);
$provider = sanitize_key((string) ($config['content_audit_provider'] ?? 'baidu'));
$missing = [];
if ($enabled && $provider === 'baidu') {
if (empty($config['content_audit_api_key'])) $missing[] = 'content_audit_api_key';
if (empty($config['content_audit_api_secret'])) $missing[] = 'content_audit_api_secret';
}
return [
'enabled' => $enabled,
'provider' => $provider,
'configured' => $enabled && ($provider === 'null' || empty($missing)),
'available' => $enabled,
'verified' => !empty($config['content_audit_verified']),
'transport' => 'http_api',
'missing_fields' => $missing,
'text_enabled' => !empty($config['content_audit_text_enabled']),
'image_enabled' => !empty($config['content_audit_image_enabled']),
'on_failure' => (string) ($config['content_audit_on_failure'] ?? 'allow'),
'log_enabled' => !empty($config['content_audit_log_enabled']),
];
}
/* ============================================================
* Provider dispatch
* ============================================================ */
private function call_text_api(string $provider, string $text, array $config): array {
switch ($provider) {
case 'baidu':
return $this->baidu_text_censor($text, $config);
default:
return $this->local_pattern_check($text, $provider);
}
}
private function call_image_api(string $provider, string $image_path, array $config): array {
switch ($provider) {
case 'baidu':
return $this->baidu_image_censor($image_path, $config);
default:
return $this->ok('no_image_provider');
}
}
/* ============================================================
* 百度内容审核 Provider
* ============================================================ */
/**
* 百度文本审核
*/
private function baidu_text_censor(string $text, array $config): array {
$api_key = trim((string) ($config['content_audit_api_key'] ?? ''));
$api_secret = trim((string) ($config['content_audit_api_secret'] ?? ''));
if ($api_key === '' || $api_secret === '') {
return ['passed' => false, 'reason' => 'config_missing', 'labels' => [], 'raw' => []];
}
$token = $this->baidu_get_access_token($api_key, $api_secret, $config);
if ($token === '') {
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => []];
}
$censor_url = trim((string) ($config['content_audit_baidu_text_url'] ?? ''));
if ($censor_url === '') {
$censor_url = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined';
}
$request_url = add_query_arg(['access_token' => $token], $censor_url);
$response = wp_remote_post($request_url, [
'timeout' => 15,
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
'body' => http_build_query(['text' => $text]),
]);
if (is_wp_error($response)) {
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['error' => $response->get_error_message()]];
}
$status_code = (int) wp_remote_retrieve_response_code($response);
if ($status_code !== 200) {
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['status' => $status_code]];
}
$payload = json_decode((string) wp_remote_retrieve_body($response), true);
if (!is_array($payload) || isset($payload['error_code'])) {
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => $payload ?: []];
}
return $this->parse_baidu_result($payload);
}
/**
* 百度图片审核
*/
private function baidu_image_censor(string $image_path, array $config): array {
$api_key = trim((string) ($config['content_audit_api_key'] ?? ''));
$api_secret = trim((string) ($config['content_audit_api_secret'] ?? ''));
if ($api_key === '' || $api_secret === '') {
return ['passed' => false, 'reason' => 'config_missing', 'labels' => [], 'raw' => []];
}
$token = $this->baidu_get_access_token($api_key, $api_secret, $config);
if ($token === '') {
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => []];
}
// 支持本地路径和 URL
if (filter_var($image_path, FILTER_VALIDATE_URL)) {
$image_data = @file_get_contents($image_path);
} else {
$image_data = @file_get_contents($image_path);
}
if ($image_data === false) {
return ['passed' => false, 'reason' => 'file_error', 'labels' => [], 'raw' => []];
}
$censor_url = trim((string) ($config['content_audit_baidu_img_url'] ?? ''));
if ($censor_url === '') {
$censor_url = 'https://aip.baidubce.com/rest/2.0/solution/v1/img_censor/v2/user_defined';
}
$request_url = add_query_arg(['access_token' => $token], $censor_url);
$response = wp_remote_post($request_url, [
'timeout' => 30,
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
'body' => http_build_query(['image' => base64_encode($image_data)]),
]);
if (is_wp_error($response)) {
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['error' => $response->get_error_message()]];
}
$status_code = (int) wp_remote_retrieve_response_code($response);
if ($status_code !== 200) {
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['status' => $status_code]];
}
$payload = json_decode((string) wp_remote_retrieve_body($response), true);
if (!is_array($payload) || isset($payload['error_code'])) {
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => $payload ?: []];
}
return $this->parse_baidu_result($payload);
}
/**
* 百度 access_token 获取(带缓存)
*/
private function baidu_get_access_token(string $api_key, string $api_secret, array $config): string {
$cache_key = 'aksrt_baidu_token_' . md5($api_key);
$cached = get_transient($cache_key);
if ($cached) return (string) $cached;
$token_url = trim((string) ($config['content_audit_baidu_token_url'] ?? ''));
if ($token_url === '') {
$token_url = 'https://aip.baidubce.com/oauth/2.0/token';
}
$request_url = add_query_arg([
'grant_type' => 'client_credentials',
'client_id' => $api_key,
'client_secret' => $api_secret,
], $token_url);
$response = wp_remote_get($request_url, ['timeout' => 10]);
if (is_wp_error($response)) return '';
$status_code = (int) wp_remote_retrieve_response_code($response);
if ($status_code !== 200) return '';
$payload = json_decode((string) wp_remote_retrieve_body($response), true);
if (!is_array($payload) || isset($payload['error'])) return '';
$token = (string) ($payload['access_token'] ?? '');
$expires = max(3600, (int) ($payload['expires_in'] ?? 3600) - 300);
if ($token !== '') {
set_transient($cache_key, $token, $expires);
}
return $token;
}
/**
* 解析百度审核结果 → 统一格式
*/
private function parse_baidu_result(array $payload): array {
$conclusion_type = isset($payload['conclusionType']) ? (int) $payload['conclusionType'] : 1;
$conclusion = (string) ($payload['conclusion'] ?? '');
$top_category = $this->baidu_extract_top_category($payload);
// conclusionType: 1=合规, 2=不合规, 3=疑似, 4=审核失败
if ($conclusion_type === 1) {
return [
'passed' => true,
'reason' => '',
'labels' => ['compliant'],
'raw' => $payload,
];
}
$score_map = [2 => 95, 3 => 70, 4 => 60];
$reason_map = [2 => 'non_compliant', 3 => 'suspected', 4 => 'review_failed'];
return [
'passed' => false,
'reason' => $reason_map[$conclusion_type] ?? 'unknown',
'labels' => [$top_category ?: ('baidu_type_' . $conclusion_type)],
'raw' => $payload,
];
}
private function baidu_extract_top_category(array $payload): string {
if (!isset($payload['data']) || !is_array($payload['data']) || empty($payload['data'][0])) {
return '';
}
$first = $payload['data'][0];
if (!is_array($first)) return '';
if (!empty($first['msg'])) return (string) $first['msg'];
if (!empty($first['type'])) return 'type_' . (string) $first['type'];
if (!empty($first['subType'])) return 'sub_' . (string) $first['subType'];
return '';
}
/* ============================================================
* 本地基础过滤(无 API 时的 fallback)
* ============================================================ */
private function local_pattern_check(string $text, string $provider): array {
$forbidden_patterns = ['/赌博/i', '/色情/i', '/枪支/i', '/毒品/i'];
foreach ($forbidden_patterns as $pattern) {
if (preg_match($pattern, $text, $m)) {
return [
'passed' => false,
'reason' => sprintf('本地敏感词匹配: %s', $m[0]),
'labels' => ['forbidden'],
'raw' => ['provider' => $provider, 'method' => 'local_pattern'],
];
}
}
return $this->ok('passthrough');
}
/* ============================================================
* Helpers
* ============================================================ */
private function ok(string $reason = ''): array {
return ['passed' => true, 'reason' => $reason, 'labels' => [], 'raw' => []];
}
private function get_config(): array {
if (class_exists('\AKSRT\Settings\Settings')) {
return \AKSRT\Settings\Settings::get_content_audit_settings();
}
return ['content_audit_enabled' => false];
}
private function log_audit(string $type, string $content, array $result): void {
$log = get_option('aksrt_content_audit_log', []);
$log[] = [
'type' => $type,
'content' => mb_substr($content, 0, 100),
'passed' => $result['passed'],
'reason' => $result['reason'] ?? '',
'time' => current_time('mysql'),
];
if (count($log) > 500) {
$log = array_slice($log, -500);
}
update_option('aksrt_content_audit_log', $log);
}
}