Files
wordpress-theme-aksrt/inc/classes/moderation-traits/trait-aksrt-moderation-part-1.php
T

405 lines
15 KiB
PHP

<?php
defined('ABSPATH') || exit;
trait AKSRT_Moderation_Part_1 {
public function render_comments_custom_column(string $column, int $comment_id): void {
if ($column !== 'aksrt_moderation') {
return;
}
$status = '';
if (function_exists('get_comment')) {
$comment_obj = get_comment($comment_id);
if (is_object($comment_obj) && isset($comment_obj->comment_approved)) {
$status = (string) $comment_obj->comment_approved;
}
}
$status_label = $this->status_label($status);
$source = (string) get_comment_meta($comment_id, 'aksrt_moderation_source', true);
$result_value = (string) get_comment_meta($comment_id, 'aksrt_moderation_result', true);
$reason = (string) get_comment_meta($comment_id, 'aksrt_moderation_reason', true);
$akismet = (string) get_comment_meta($comment_id, 'aksrt_moderation_akismet', true);
$ai_score = (string) get_comment_meta($comment_id, 'aksrt_moderation_ai_score', true);
$ai_flagged = (string) get_comment_meta($comment_id, 'aksrt_moderation_ai_flagged', true);
$ai_top = (string) get_comment_meta($comment_id, 'aksrt_moderation_ai_top_category', true);
$keyword = (string) get_comment_meta($comment_id, 'aksrt_moderation_keyword', true);
$match_type = (string) get_comment_meta($comment_id, 'aksrt_moderation_keyword_match_type', true);
$moderation_user_id = (string) get_comment_meta($comment_id, 'aksrt_moderation_user_id', true);
$moderation_user_ip = (string) get_comment_meta($comment_id, 'aksrt_moderation_user_ip', true);
if ($source === '') {
$source = 'wp_default';
}
if ($result_value === '') {
$result_value = $this->result_by_approved($status);
}
if ($reason === '') {
$reason = 'wp_default_policy';
}
if ($akismet === '') {
$akismet_meta = get_comment_meta($comment_id, 'akismet_result', true);
if ((string) $akismet_meta === 'true') {
$akismet = 'spam';
} elseif ((string) $akismet_meta === 'false') {
$akismet = 'ham';
} else {
$akismet = 'unknown';
}
}
if ($source === 'wp_default' && $akismet === 'ham') {
$source = 'akismet';
if ($result_value === 'wp_default_policy') {
$result_value = 'approved';
}
if ($reason === 'wp_default_policy') {
$reason = 'ham';
}
} elseif ($source === 'wp_default' && $akismet === 'spam') {
$source = 'akismet';
$result_value = 'spam';
if ($reason === 'wp_default_policy') {
$reason = 'spam';
}
}
echo '<strong>' . esc_html($status_label) . '</strong><br>';
echo 'Source: ' . esc_html($source) . ' | ';
echo 'Result: ' . esc_html($result_value) . ' | ';
echo 'Reason: ' . esc_html($reason) . ' | ';
echo 'Akismet: ' . esc_html($akismet);
$extras = [];
if ($ai_score !== '') {
$extras[] = 'AI score: ' . $ai_score;
}
if ($ai_flagged !== '') {
$extras[] = 'AI flagged: ' . $ai_flagged;
}
if ($ai_top !== '') {
$extras[] = 'AI top: ' . $ai_top;
}
if ($keyword !== '') {
$extras[] = 'Keyword: ' . $keyword;
}
if ($match_type !== '') {
$extras[] = 'Match: ' . $match_type;
}
if ($moderation_user_id !== '') {
$extras[] = 'UID: ' . $moderation_user_id;
}
if ($moderation_user_ip !== '') {
$extras[] = 'IP: ' . $moderation_user_ip;
}
if (!empty($extras)) {
echo '<br><small>' . esc_html(implode(' | ', $extras)) . '</small>';
}
}
private function new_default_result(): array {
return [
'result' => 'approved',
'source' => 'wp_default',
'reason' => 'wp_default_policy',
'akismet' => 'unknown',
'ai_score' => '',
'ai_flagged' => '',
'ai_top_category' => '',
'keyword' => '',
'keyword_match_type' => '',
'ip' => '',
];
}
private function remember_result(array $commentdata, array $decision): void {
$fp = $this->comment_fingerprint($commentdata);
if ($fp === '') {
return;
}
$this->moderation_result_map[$fp] = $decision;
if (array_key_exists('comment_approved', $commentdata)) {
$this->forced_approved_map[$fp] = $commentdata['comment_approved'];
}
}
private function resolve_result_from_runtime_or_default(array $commentdata, WP_Comment $comment): array {
$fp_data = $this->comment_fingerprint($commentdata);
if ($fp_data !== '' && isset($this->moderation_result_map[$fp_data])) {
return $this->moderation_result_map[$fp_data];
}
$fp_comment = $this->comment_fingerprint((array) $comment);
if ($fp_comment !== '' && isset($this->moderation_result_map[$fp_comment])) {
return $this->moderation_result_map[$fp_comment];
}
$fallback = $this->new_default_result();
$fallback['result'] = $this->result_by_approved((string) $comment->comment_approved);
$fallback['reason'] = 'wp_default_policy';
$akismet_meta = get_comment_meta($comment->comment_ID, 'akismet_result', true);
if ((string) $akismet_meta === 'true') {
$fallback['source'] = 'akismet';
$fallback['reason'] = 'spam';
$fallback['akismet'] = 'spam';
$fallback['result'] = 'spam';
} elseif ((string) $akismet_meta === 'false') {
$fallback['source'] = 'akismet';
$fallback['reason'] = 'ham';
$fallback['akismet'] = 'ham';
}
return $fallback;
}
private function enrich_moderation_result(array $result, array $commentdata): array {
if (!isset($result['result']) || $result['result'] === '') {
$approved = isset($commentdata['comment_approved']) ? (string) $commentdata['comment_approved'] : '1';
$result['result'] = $this->result_by_approved($approved);
}
if (!isset($result['source']) || $result['source'] === '') {
$result['source'] = 'wp_default';
}
if (!isset($result['reason']) || $result['reason'] === '') {
$result['reason'] = 'wp_default_policy';
}
if (!isset($result['akismet']) || $result['akismet'] === '') {
$result['akismet'] = 'unknown';
}
if (!isset($result['ai_score'])) {
$result['ai_score'] = '';
}
if (!isset($result['ai_flagged'])) {
$result['ai_flagged'] = '';
}
if (!isset($result['ai_top_category'])) {
$result['ai_top_category'] = '';
}
if (!isset($result['keyword'])) {
$result['keyword'] = '';
}
if (!isset($result['keyword_match_type'])) {
$result['keyword_match_type'] = '';
}
return $result;
}
private function result_by_approved(string $approved): string {
if ($approved === 'spam') {
return 'spam';
}
if ($approved === '0') {
return 'pending';
}
if ($approved === 'trash') {
return 'trash';
}
return 'approved';
}
private function status_label(string $approved): string {
if ($approved === '1') {
return 'Approved';
}
if ($approved === '0') {
return 'Pending';
}
if ($approved === 'spam') {
return 'Spam';
}
if ($approved === 'trash') {
return 'Trash';
}
return ucfirst($approved);
}
private function comment_fingerprint(array $data): string {
$post_id = (string) ($data['comment_post_ID'] ?? '');
$author = strtolower(trim((string) ($data['comment_author'] ?? '')));
$email = strtolower(trim((string) ($data['comment_author_email'] ?? '')));
$content_md5 = md5(trim((string) ($data['comment_content'] ?? '')));
$ip = trim((string) ($data['comment_author_IP'] ?? ($data['comment_author_ip'] ?? '')));
if ($post_id === '' && $author === '' && $email === '' && $content_md5 === md5('')) {
return '';
}
return md5(implode('|', [$post_id, $author, $email, $content_md5, $ip]));
}
// ============================================================
// 1. Akismet
// ============================================================
private function check_akismet(array $data, array $config, string $ip) {
if (empty($config['akismet_enabled'])) {
return true;
}
$api_key = trim((string) ($config['akismet_api_key'] ?? ''));
if ($api_key === '') {
return 'error';
}
$site_url = !empty($config['akismet_site_url']) ? (string) $config['akismet_site_url'] : home_url('/');
$payload = [
'blog' => $site_url,
'blog_lang' => $config['akismet_blog_lang'] ?? 'zh-CN',
'blog_charset' => get_bloginfo('charset') ?: 'UTF-8',
'user_ip' => $ip,
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'referrer' => $_SERVER['HTTP_REFERER'] ?? '',
'comment_type' => 'comment',
'comment_author' => $data['comment_author'] ?? '',
'comment_author_email' => $data['comment_author_email'] ?? '',
'comment_author_url' => $data['comment_author_URL'] ?? ($data['comment_author_url'] ?? ''),
'comment_content' => $data['comment_content'] ?? '',
'is_test' => defined('WP_DEBUG') && WP_DEBUG ? '1' : '0',
];
$endpoint = sprintf('https://%s.rest.akismet.com/1.1/comment-check', $api_key);
$response = wp_remote_post($endpoint, [
'body' => http_build_query($payload),
'timeout' => 10,
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
]);
if (is_wp_error($response)) {
return 'error';
}
$body = trim((string) wp_remote_retrieve_body($response));
if ($body === 'true') {
return 'spam';
}
if ($body === 'false') {
return 'ham';
}
return 'error';
}
// ============================================================
// 2. AI Moderation
// ============================================================
private function check_ai_moderation(array $data, array $config) {
if (empty($config['ai_enabled']) || empty($config['ai_moderate_comments'])) {
return true;
}
$content = trim((string) ($data['comment_content'] ?? ''));
if ($content === '') {
return true;
}
// 委托给统一内容审核服务
if (!class_exists('AKSRT_Content_Audit')) {
return true;
}
$result = AKSRT_Content_Audit::get_instance()->audit_text($content);
if ($result['passed']) {
return true;
}
// 映射到审核风控的 score 模型
$score = 70;
if (in_array('non_compliant', $result['labels'] ?? [], true)) {
$score = 95;
}
return [
'flagged' => 1,
'score' => $score,
'top_category' => $result['reason'] ?? 'content_audit_flagged',
];
}
/**
* 图片审核 — 委托给统一内容审核服务。
*
* @param string $image_path Absolute path to the local image file.
* @param array $config Optional config override (merged with get_config()).
* @return array ['passed' => bool, 'reason' => string, ...]
*/
public function baidu_moderate_image(string $image_path, array $config = []): array {
$config = array_merge($this->get_config(), $config);
if (empty($config['ai_enabled']) || empty($config['ai_moderate_background'])) {
return ['passed' => true, 'reason' => 'disabled'];
}
if (!class_exists('AKSRT_Content_Audit')) {
return ['passed' => true, 'reason' => 'audit_service_missing'];
}
return AKSRT_Content_Audit::get_instance()->audit_image($image_path);
}
// ============================================================
// 3. Blocked keywords
// ============================================================
private function check_blocked_keywords(array $data, array $config): array {
$keywords_raw = (string) ($config['blocked_keywords'] ?? '');
if (trim($keywords_raw) === '') {
return ['hit' => false, 'keyword' => '', 'match_type' => ''];
}
$parts = preg_split('/[\r\n,;、\x{FF0C}\x{FF1B}]+/u', $keywords_raw);
$keywords = array_values(array_filter(array_map('trim', (array) $parts), static function ($v) {
return $v !== '';
}));
if (empty($keywords)) {
return ['hit' => false, 'keyword' => '', 'match_type' => ''];
}
$text = implode(' ', [
(string) ($data['comment_content'] ?? ''),
(string) ($data['comment_author'] ?? ''),
(string) ($data['comment_author_email'] ?? ''),
(string) ($data['comment_author_URL'] ?? ($data['comment_author_url'] ?? '')),
]);
$text_norm = $this->normalize_text($text);
foreach ($keywords as $keyword) {
if ($keyword === '') {
continue;
}
// User-defined regex keyword like /pattern/flags.
if (preg_match('#^/.+/[imsxuADSUXJ]*$#', $keyword)) {
$matched = @preg_match($keyword, $text);
if ($matched === 1) {
return ['hit' => true, 'keyword' => $keyword, 'match_type' => 'regex'];
}
continue;
}
if ($this->contains_text($text, $keyword)) {
return ['hit' => true, 'keyword' => $keyword, 'match_type' => 'plain'];
}
$keyword_norm = $this->normalize_text($keyword);
if ($keyword_norm !== '' && $this->contains_text($text_norm, $keyword_norm)) {
return ['hit' => true, 'keyword' => $keyword, 'match_type' => 'normalized'];
}
}
return ['hit' => false, 'keyword' => '', 'match_type' => ''];
}
private function contains_text(string $haystack, string $needle): bool {
if ($needle === '') {
return false;
}
if (function_exists('mb_stripos')) {
return mb_stripos($haystack, $needle, 0, 'UTF-8') !== false;
}
return stripos($haystack, $needle) !== false;
}
}