refactor(classes): organize inc/classes into domain folders

Move ~94 root class shells and root *-traits into domain directories
(core, auth, user, credits, content, payment/gateways, forum, shop,
sms, ai, messages, extensions, widgets, settings, admin). Update
bootstrap require paths, co-locate traits with owners, fix forum/user
relative requires, drop duplicate shop controller requires, and wire
home-modules load. Path-only; class names and init order unchanged.
This commit is contained in:
kate522 committed 2026-07-17 11:11:57 +08:00
1 parent 2a8fe68048
commit ce1c13b947
115 files changed
+171 -224

No files matched your search

@@ -0,0 +1,404 @@
<?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;
}
}
@@ -0,0 +1,163 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_Moderation_Part_2 {
private function normalize_text(string $text): string {
$text = preg_replace('/[\x{200B}-\x{200D}\x{FEFF}]/u', '', $text);
$text = preg_replace('/\s+/u', '', (string) $text);
if (function_exists('mb_strtolower')) {
return mb_strtolower((string) $text, 'UTF-8');
}
return strtolower((string) $text);
}
// ============================================================
// 4. Rate limit
// ============================================================
private function check_rate_limit(array $data, array $config, string $ip) {
if (empty($config['rate_limit_enabled'])) {
return true;
}
$min_interval = max(1, (int) ($config['rate_limit_min_interval'] ?? 8));
$max_per_hour = max(1, (int) ($config['rate_limit_max_per_hour'] ?? 15));
$prefix = 'aksrt_rate_';
$last_key = $prefix . md5($ip . '_last');
$hour_key = $prefix . md5($ip . '_hourly');
$exp_key = '_' . $prefix . md5($ip . '_hourly_expiry');
$last_comment_time = get_transient($last_key);
if ($last_comment_time !== false) {
$elapsed = time() - (int) $last_comment_time;
if ($elapsed < $min_interval) {
$wait_time = $min_interval - $elapsed;
return sprintf(__('You are commenting too fast. Please wait %d seconds.', 'aksrt'), $wait_time);
}
}
$hourly_count = get_transient($hour_key);
if ($hourly_count !== false && (int) $hourly_count >= $max_per_hour) {
$reset_time = (int) (get_transient($exp_key) ?: time());
$remaining = max(0, $reset_time - time());
$minutes = (int) ceil($remaining / 60);
return sprintf(__('You have reached the hourly comment limit. Try again in %d minutes.', 'aksrt'), $minutes);
}
if ($hourly_count === false) {
set_transient($hour_key, 1, HOUR_IN_SECONDS);
set_transient($exp_key, time() + HOUR_IN_SECONDS, HOUR_IN_SECONDS);
} else {
set_transient($hour_key, ((int) $hourly_count) + 1, HOUR_IN_SECONDS);
}
set_transient($last_key, time(), $min_interval);
return true;
}
// ============================================================
// 5. GeoIP
// ============================================================
private function check_geoip(array $config, string $ip) {
if (empty($config['geoip_enabled'])) {
return true;
}
$block_countries = $config['geoip_block_countries'] ?? [];
if (!is_array($block_countries) || empty($block_countries)) {
return true;
}
$country_code = $this->lookup_geoip($ip, $config);
if ($country_code === null) {
return true;
}
$country_code = strtoupper($country_code);
$block_upper = array_map('strtoupper', $block_countries);
if (in_array($country_code, $block_upper, true)) {
return sprintf(__('Comments from your region (%s) are currently blocked.', 'aksrt'), esc_html($country_code));
}
return true;
}
private function lookup_geoip(string $ip, array $config): ?string {
// 优先复用 IP 归属地模块的查询能力(含缓存)
if (class_exists('AKSRT_IP_Geo')) {
$result = AKSRT_IP_Geo::get_instance()->lookup($ip);
if (!empty($result['country'])) {
return $result['country'];
}
}
// 回退到内置查询
$ip_geo_config = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_ip_geo_settings()
: [];
$provider = (string) ($ip_geo_config['ip_geo_provider'] ?? $config['geoip_provider'] ?? 'ip-api.com');
$api_key = (string) ($ip_geo_config['ip_geo_api_key'] ?? $config['geoip_api_key'] ?? '');
if ($provider === 'ipinfo' || $provider === 'ipinfo.io') {
return $this->geoip_ipinfo($ip, $api_key);
}
return $this->geoip_ipapi($ip);
}
/**
* Free ip-api.com endpoint.
*/
private function geoip_ipapi(string $ip): ?string {
$url = 'http://ip-api.com/json/' . urlencode($ip) . '?fields=status,countryCode,message';
$response = wp_remote_get($url, [
'timeout' => 5,
'headers' => ['Accept' => 'application/json'],
]);
if (is_wp_error($response)) {
return null;
}
$body = json_decode((string) wp_remote_retrieve_body($response), true);
if (($body['status'] ?? '') === 'success') {
return $body['countryCode'] ?? null;
}
return null;
}
private function geoip_ipinfo(string $ip, string $api_key): ?string {
$url = 'https://ipinfo.io/' . urlencode($ip) . '/json?token=' . urlencode($api_key ?: '');
$response = wp_remote_get($url, [
'timeout' => 5,
'headers' => ['Accept' => 'application/json'],
]);
if (is_wp_error($response)) {
return null;
}
$body = json_decode((string) wp_remote_retrieve_body($response), true);
return $body['country'] ?? null;
}
// ============================================================
// Utilities
// ============================================================
private function get_client_ip(): string {
$keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'];
foreach ($keys as $key) {
if (!empty($_SERVER[$key])) {
$parts = explode(',', (string) $_SERVER[$key]);
$ip = trim($parts[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
}