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).
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Auth;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait ModerationPart1 {
|
||||
|
||||
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\Extensions\ContentAudit')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$result = \AKSRT\Extensions\ContentAudit::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\Extensions\ContentAudit')) {
|
||||
return ['passed' => true, 'reason' => 'audit_service_missing'];
|
||||
}
|
||||
|
||||
return \AKSRT\Extensions\ContentAudit::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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user