refactor(autoload): migrate theme classes to Composer PSR-4

Move 288 types from global AKSRT_* + inc/classes manual requires into
namespace AKSRT\{Domain} under src/AKSRT/, wire composer.json psr-4,
and slim bootstrap to helpers/modules/hooks only. Split multi-class
widget/gateway files, update admin contract audit paths, and document
the layout in ADR/README.
This commit is contained in:
2026-07-17 13:29:22 +08:00
parent 8f349b40fb
commit aaf458af1c
404 changed files with 13602 additions and 7705 deletions
+382
View File
@@ -0,0 +1,382 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
trait TopicServicePart2 {
/**
* Prepare vote meta from request data.
*/
public function prepare_vote_meta(array $data): array {
$enabled = !empty($data['vote_enabled']);
$title = sanitize_text_field((string) ($data['vote_title'] ?? ''));
$multiple = !empty($data['vote_multiple']);
$raw_options = $data['vote_options'] ?? $data['vote_options_text'] ?? [];
$labels = [];
if (is_array($raw_options)) {
foreach ($raw_options as $option) {
$option = sanitize_text_field((string) $option);
if ($option !== '') {
$labels[] = $option;
}
}
} else {
$lines = preg_split('/\r\n|\r|\n/', (string) $raw_options) ?: [];
foreach ($lines as $line) {
$line = sanitize_text_field((string) $line);
if ($line !== '') {
$labels[] = $line;
}
}
}
$labels = array_values(array_unique($labels));
$config = $this->forum->get_config();
$vote_max = (int) ($config['other']['vote_max'] ?? 8);
if ($vote_max > 0 && count($labels) > $vote_max) {
$labels = array_slice($labels, 0, $vote_max);
}
$vote_type = sanitize_key((string) ($data['vote_type'] ?? 'standard'));
if ($vote_type === 'pk' && count($labels) !== 2) {
$vote_type = 'standard';
}
if ($vote_type === 'pk') {
$multiple = false;
}
if (count($labels) < 2) {
$enabled = false;
}
$expiry_days = (int) ($data['vote_expiry_days'] ?? 0);
$options = [];
foreach ($labels as $index => $label) {
$options[] = [
'id' => 'option_' . ($index + 1),
'label' => $label,
'count' => 0,
];
}
return [
'enabled' => $enabled,
'title' => $title,
'multiple' => $multiple,
'vote_type' => $vote_type,
'expiry_days' => $expiry_days,
'options' => $options,
];
}
/**
* Save vote meta while preserving counts when possible.
*/
public function save_vote_meta(int $topic_id, array $vote_meta): void {
$existing_options = get_post_meta($topic_id, '_aksrt_vote_options', true);
$existing_options = is_array($existing_options) ? $existing_options : [];
$existing_by_label = [];
foreach ($existing_options as $existing_option) {
if (!empty($existing_option['label'])) {
$existing_by_label[(string) $existing_option['label']] = $existing_option;
}
}
$options = [];
foreach ($vote_meta['options'] as $index => $option) {
$label = (string) ($option['label'] ?? '');
if ($label === '') {
continue;
}
$existing = $existing_by_label[$label] ?? null;
$options[] = [
'id' => $existing['id'] ?? ('option_' . ($index + 1)),
'label' => $label,
'count' => (int) ($existing['count'] ?? 0),
];
}
update_post_meta($topic_id, '_aksrt_vote_enabled', !empty($vote_meta['enabled']) ? '1' : '0');
update_post_meta($topic_id, '_aksrt_vote_title', sanitize_text_field((string) ($vote_meta['title'] ?? '')));
update_post_meta($topic_id, '_aksrt_vote_multiple', !empty($vote_meta['multiple']) ? '1' : '0');
update_post_meta($topic_id, '_aksrt_vote_options', $options);
$vote_type = in_array($vote_meta['vote_type'] ?? '', ['standard', 'pk'], true) ? $vote_meta['vote_type'] : 'standard';
update_post_meta($topic_id, '_aksrt_vote_type', $vote_type);
$expiry_days = (int) ($vote_meta['expiry_days'] ?? 0);
if (!in_array($expiry_days, [0, 1, 7, 30], true)) {
$expiry_days = 0;
}
update_post_meta($topic_id, '_aksrt_vote_expiry_days', $expiry_days);
if ($expiry_days > 0 && !get_post_meta($topic_id, '_aksrt_vote_started_at', true)) {
update_post_meta($topic_id, '_aksrt_vote_started_at', current_time('mysql'));
}
if (empty($vote_meta['enabled'])) {
delete_post_meta($topic_id, '_aksrt_vote_records');
}
}
/**
* Get vote payload for a topic.
*/
public function get_vote_payload(int $topic_id, int $user_id = 0): array {
$enabled = get_post_meta($topic_id, '_aksrt_vote_enabled', true) === '1';
$options = get_post_meta($topic_id, '_aksrt_vote_options', true);
$options = is_array($options) ? $options : [];
$records = get_post_meta($topic_id, '_aksrt_vote_records', true);
$records = is_array($records) ? $records : [];
$vote_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_vote_type', true));
if (!in_array($vote_type, ['standard', 'pk'], true)) {
$vote_type = 'standard';
}
$expiry_days = (int) get_post_meta($topic_id, '_aksrt_vote_expiry_days', true);
$started_at = (string) get_post_meta($topic_id, '_aksrt_vote_started_at', true);
$expired = false;
if ($expiry_days > 0 && $started_at !== '') {
$expire_time = strtotime($started_at) + ($expiry_days * DAY_IN_SECONDS);
$expired = time() >= $expire_time;
}
$config = $this->forum->get_config();
$display_mode = $config['other']['vote_number_type'] ?? 'percent';
$selected = $user_id > 0 && isset($records[(string) $user_id]) && is_array($records[(string) $user_id])
? array_values(array_map('strval', $records[(string) $user_id]))
: [];
$total_votes = 0;
foreach ($options as &$option) {
$count = max(0, (int) ($option['count'] ?? 0));
$option['count'] = $count;
$option['selected'] = in_array((string) ($option['id'] ?? ''), $selected, true);
$total_votes += $count;
}
unset($option);
return [
'enabled' => $enabled && !empty($options),
'title' => (string) get_post_meta($topic_id, '_aksrt_vote_title', true),
'multiple' => get_post_meta($topic_id, '_aksrt_vote_multiple', true) === '1',
'vote_type' => $vote_type,
'expiry_days' => $expiry_days,
'expired' => $expired,
'display_mode' => $display_mode,
'options' => $options,
'total_votes' => $total_votes,
'has_voted' => !empty($selected),
'selected' => $selected,
];
}
// ================================================================
// Content processing
// ================================================================
/**
* Save governance meta fields from an editable payload.
*/
public function save_topic_governance_meta(int $topic_id, array $data): void {
if (isset($data['visibility'])) {
$visibility = sanitize_key((string) $data['visibility']);
if (isset($this->get_visibility_map()[$visibility])) {
update_post_meta($topic_id, '_aksrt_visibility', $visibility);
}
}
if (array_key_exists('special_topic', $data)) {
update_post_meta($topic_id, '_aksrt_special_topic', sanitize_text_field((string) $data['special_topic']));
}
if (array_key_exists('vote_enabled', $data) || array_key_exists('vote_options', $data) || array_key_exists('vote_options_text', $data)) {
$this->save_vote_meta($topic_id, $this->prepare_vote_meta($data));
}
}
public function save_cover_meta(int $post_id, array $data): void {
$cover_type = sanitize_key((string) ($data['cover_type'] ?? ''));
if (!in_array($cover_type, ['image', 'slide', 'video'], true)) {
delete_post_meta($post_id, '_aksrt_cover_type');
return;
}
update_post_meta($post_id, '_aksrt_cover_type', $cover_type);
if ($cover_type === 'image') {
$url = esc_url_raw((string) ($data['cover_image'] ?? ''));
update_post_meta($post_id, '_aksrt_cover_image', $url);
} elseif ($cover_type === 'slide') {
$raw = $data['featured_slide'] ?? '';
$slides = is_array($raw) ? $raw : json_decode((string) $raw, true);
if (!is_array($slides)) {
$slides = [];
}
$slides = array_values(array_filter(array_map('esc_url_raw', array_slice($slides, 0, 9))));
update_post_meta($post_id, '_aksrt_featured_slide', $slides);
} elseif ($cover_type === 'video') {
$url = esc_url_raw((string) ($data['featured_video'] ?? ''));
update_post_meta($post_id, '_aksrt_featured_video', $url);
}
}
/**
* Sanitize topic content based on user's compose capabilities.
*
* Strips <img>, <video>, <audio>, <iframe>, [aksrt_hide] blocks
* when the author lacks the corresponding permission.
*/
public function sanitize_topic_content(int $user_id, string $content): string {
$caps = $this->forum->get_compose_capabilities($user_id);
if (empty($caps['can_upload_image'])) {
$content = preg_replace('/<img[^>]*>/i', '', $content);
}
if (empty($caps['can_upload_video'])) {
$content = preg_replace('/<video[^>]*>.*?<\/video>/is', '', $content);
$content = preg_replace('/<audio[^>]*>.*?<\/audio>/is', '', $content);
}
if (empty($caps['can_iframe_video'])) {
$content = preg_replace('/<iframe[^>]*>.*?<\/iframe>/is', '', $content);
}
if (empty($caps['can_set_hide'])) {
$content = preg_replace('/\[aksrt_hide[^\]]*\](.*?)\[\/aksrt_hide\]/s', '$1', $content);
}
return $content;
}
/**
* Register the [aksrt_hide] shortcode for hidden content blocks.
*/
public static function register_hide_shortcode(): void {
add_shortcode('aksrt_hide', [self::class, 'render_hide_shortcode']);
}
/**
* Render [aksrt_hide] shortcode.
*
* Attributes:
* type — 'login' (default) | 'comment' | 'role' | 'credits'
* roles — comma-separated role slugs (for type=role)
* cost — int credits cost (for type=credits)
*/
public static function render_hide_shortcode($atts, $content = ''): string {
$atts = shortcode_atts([
'type' => 'login',
'roles' => '',
'cost' => 0,
], $atts, 'aksrt_hide');
$type = sanitize_key($atts['type']);
$user_id = get_current_user_id();
$visible = false;
$message = '';
switch ($type) {
case 'login':
$visible = $user_id > 0;
if (!$visible) {
$message = __('此内容需要登录后才能查看。', 'aksrt');
}
break;
case 'comment':
if ($user_id <= 0) {
$message = __('登录并评论后可查看隐藏内容。', 'aksrt');
} else {
$post_id = get_the_ID();
if ($post_id && function_exists('get_comments')) {
$has_commented = (int) get_comments([
'post_id' => $post_id,
'user_id' => $user_id,
'count' => true,
'status' => 'approve',
]) > 0;
$visible = $has_commented;
}
if (!$visible) {
$message = __('评论后可查看隐藏内容。', 'aksrt');
}
}
break;
case 'role':
if ($user_id <= 0) {
$message = __('此内容仅特定角色可见。', 'aksrt');
} else {
$allowed = array_filter(array_map('trim', explode(',', (string) $atts['roles'])));
if (empty($allowed)) {
$visible = true;
} else {
$user = get_userdata($user_id);
$visible = $user && !empty(array_intersect($user->roles, $allowed));
}
if (!$visible) {
$message = __('此内容仅限特定用户角色查看。', 'aksrt');
}
}
break;
case 'credits':
$cost = max(0, (int) $atts['cost']);
if ($user_id <= 0) {
$message = sprintf(__('此内容需要 %d 积分解锁。请先登录。', 'aksrt'), $cost);
} elseif ($cost <= 0) {
$visible = true;
} else {
$balance = class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::get_balance($user_id) : 0;
$visible = $balance >= $cost;
if (!$visible) {
$message = sprintf(__('此内容需要 %d 积分解锁(当前余额:%d)。', 'aksrt'), $cost, (int) $balance);
}
}
break;
default:
$visible = $user_id > 0;
if (!$visible) {
$message = __('此内容需要登录后才能查看。', 'aksrt');
}
}
if ($visible) {
return '<div class="aksrt-forum-hide aksrt-forum-hide--revealed">' . do_shortcode($content) . '</div>';
}
return '<div class="aksrt-forum-hide aksrt-forum-hide--locked">'
. '<div class="aksrt-forum-hide__message">'
. '<i class="fas fa-lock"></i> '
. esc_html($message)
. '</div></div>';
}
/**
* Split a tag string.
*/
public function split_tags(string $tags): array {
$parts = preg_split('/[,,]/', $tags) ?: [];
$clean = [];
foreach ($parts as $part) {
$part = sanitize_text_field(trim($part));
if ($part !== '') {
$clean[] = $part;
}
}
return array_values(array_unique($clean));
}
}