Files
wordpress-theme-aksrt/src/AKSRT/Forum/TopicServicePart2.php
T
kate522 aaf458af1c 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.
2026-07-17 13:29:22 +08:00

383 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\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));
}
}