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.
383 lines
14 KiB
PHP
383 lines
14 KiB
PHP
<?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));
|
||
}
|
||
}
|