Files
wordpress-theme-aksrt/inc/helpers/aksrt-single-components.php
T
kate522 7597972eea feat(user-ban): frontend ban modal replaces the backend restrictions tab
1:1 replicate the reference theme user-ban flow on the frontend:

- Ban/unban modal rendered by inc/helpers/user-ban.php, opened via
  AksrtModalRuntime.openRemote (600px desktop dialog, 360px initial
  height, mobile bottom drawer, c-yellow colorful unban header).
- Three entry points wired: user homepage cover three-dot menu,
  comment section menu, article author card menu (ban item takes
  priority over report, matching the reference hook order).
- REST endpoints /user-ban/modal and /user-ban/set under aksrt/v1,
  guarded by check_permission, persisted through User\Restriction.
- Submit semantics match zib_ajax_set_user_ban: ban reloads the page,
  unban only closes the modal.

Cleanup (no dual-track): the backend ban-restrictions tab, its form
handlers and flash keys are removed; ticket processing links to the
frontend user homepage as the reference theme does; admin/report and
appeal notification URLs move to type/status query args.

Verification: 337/337 widget-system contract tests, 41/41 admin and
front-submit tests, php -l clean on all changed PHP files.
2026-09-20 18:42:53 +08:00

535 lines
19 KiB
PHP

<?php
/**
* Single-post component helpers (legacy parity).
*
* Meta box, subtitle, status badge, allow-view gate, colored tags, related cards.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_get_the_subtitle')) :
/**
* Post subtitle (meta `_subtitle`), legacy get_the_subtitle style.
*
* @param bool $span Wrap in focus-color span.
* @param string $class Extra class on the span.
* @param int|null $post_id Post ID.
*/
function aksrt_get_the_subtitle(bool $span = true, string $class = '', $post_id = null): string {
$post_id = $post_id ? (int) $post_id : (int) get_the_ID();
if ($post_id <= 0) {
return '';
}
// New route: subtitle lives in \AKSRT\Core\PostMetaBox aggregate meta.
$subtitle = '';
if (class_exists('\AKSRT\Core\PostMetaBox')) {
$meta = \AKSRT\Core\PostMetaBox::get_meta($post_id);
$subtitle = trim((string) ($meta['subtitle'] ?? ''));
}
if ($subtitle === '') {
return '';
}
if (!$span) {
return $subtitle;
}
$cls = trim('focus-color ' . $class);
return ' <span class="' . esc_attr($cls) . '">' . esc_html($subtitle) . '</span>';
}
endif;
if (!function_exists('aksrt_get_single_title_html')) :
/**
* Title + optional subtitle for single headers.
*/
function aksrt_get_single_title_html($post_id = null): string {
$post_id = $post_id ? (int) $post_id : (int) get_the_ID();
$title = get_the_title($post_id);
$sub = function_exists('aksrt_get_the_subtitle') ? aksrt_get_the_subtitle(true, '', $post_id) : '';
return esc_html($title) . $sub;
}
endif;
if (!function_exists('aksrt_get_posts_meta_items_html')) :
/**
* legacy-like post meta pills: comment + views + likes.
*
* @param int|null $post_id Post ID.
*/
function aksrt_get_posts_meta_items_html($post_id = null): string {
$post = get_post($post_id);
if (!($post instanceof WP_Post)) {
return '';
}
$post_id = (int) $post->ID;
$html = '';
$comments_closed = function_exists('aksrt_are_comments_closed')
? aksrt_are_comments_closed()
: (!comments_open($post_id));
if (comments_open($post_id) && !$comments_closed) {
$href = is_single($post_id)
? 'javascript:(typeof scrollTopTo===\'function\'?scrollTopTo(\'#comments\'):void(0));'
: get_comments_link($post_id);
$count = (int) get_comments_number($post_id);
$html .= '<span class="item meta-comm"><a rel="nofollow" href="' . esc_attr($href) . '" title="' . esc_attr__('去评论', 'aksrt') . '">'
. '<i class="fa fas fa-comment" aria-hidden="true"></i> ' . esc_html((string) $count) . '</a></span>';
}
$views = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post_id) : 0;
$views_label = function_exists('aksrt_format_number') ? aksrt_format_number($views) : number_format_i18n($views);
$html .= '<span class="item meta-view"><i class="fa fas fa-eye" aria-hidden="true"></i> ' . esc_html((string) $views_label) . '</span>';
$like_on = true;
if (class_exists('\AKSRT\Settings\Settings')) {
$page = \AKSRT\Settings\Settings::get_post_page_settings();
$like_on = !isset($page['like_enabled']) || !empty($page['like_enabled']);
}
if ($like_on && $post->post_type === 'post') {
$likes = function_exists('aksrt_get_post_likes') ? (int) aksrt_get_post_likes($post_id) : 0;
$html .= '<span class="item meta-like"><i class="fa fas fa-heart" aria-hidden="true"></i> ' . esc_html((string) $likes) . '</span>';
}
return $html;
}
endif;
if (!function_exists('aksrt_get_single_meta_box')) :
/**
* legacy equivalent.
*
* @param int|null $post_id Post ID.
*/
function aksrt_get_single_meta_box($post_id = null): string {
if (!is_single() && !is_singular()) {
return '';
}
$post_id = $post_id ? (int) $post_id : (int) get_the_ID();
$meta = function_exists('aksrt_get_posts_meta_items_html') ? aksrt_get_posts_meta_items_html($post_id) : '';
$more = '';
$edit_link = '';
if (current_user_can('manage_options')) {
// Site administrators retain the native editor for full post maintenance.
$edit_link = get_edit_post_link($post_id);
} elseif (class_exists('\AKSRT\Core\FrontSubmit') && function_exists('aksrt_find_template_page')) {
$front_submit = \AKSRT\Core\FrontSubmit::get_instance();
$submit_page = aksrt_find_template_page('templates/pages/page-submit.php');
if ($submit_page instanceof WP_Post && $front_submit->can_edit_post($post_id)) {
// Do not fall back to /wp-admin or a guessed submit URL. A normal
// author sees this action only when the configured frontend editor exists.
$edit_link = function_exists('aksrt_get_new_post_url')
? aksrt_get_new_post_url($post_id)
: '';
}
}
$permalink = get_permalink($post_id);
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url($permalink ?: home_url('/'));
$menu_id = 'post-drop-meta-menu-' . $post_id;
$more .= '<div class="clearfix ml6 post-drop-meta">';
$more .= '<button type="button" class="but cir post-drop-meta__toggle" aria-haspopup="true" aria-controls="' . esc_attr($menu_id) . '" aria-expanded="false" aria-label="' . esc_attr__('更多', 'aksrt') . '">';
$more .= '<i class="fa fas fa-ellipsis-h" aria-hidden="true"></i></button>';
$more .= '<div id="' . esc_attr($menu_id) . '" class="post-drop-meta__menu" aria-hidden="true">';
// 首项(参考主题 zib_get_post_more_dropdown 顺序):管理员 → 禁封用户;
// 无禁封权限时不占位,由下方举报项兜底(「禁封优先、否则举报」互斥语义)。
$ban_item = function_exists('aksrt_get_post_author_ban_item')
? aksrt_get_post_author_ban_item((int) get_post_field('post_author', $post_id), (string) $permalink)
: '';
$more .= $ban_item; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper escapes internally.
if ($edit_link) {
$more .= '<a href="' . esc_url($edit_link) . '" class="post-drop-meta__item"><i class="fa fas fa-edit" aria-hidden="true"></i><span>' . esc_html__('编辑文章', 'aksrt') . '</span></a>';
}
$more .= '<button type="button" class="post-drop-meta__item" data-share-copy data-share-url="' . esc_url((string) $permalink) . '"><i class="fa fas fa-link" aria-hidden="true"></i><span>' . esc_html__('复制链接', 'aksrt') . '</span></button>';
if ($ban_item === '' && is_user_logged_in()) {
$report_payload = wp_json_encode([
'target_type' => 'post',
'target_id' => (int) $post_id,
'target_name' => get_the_title($post_id),
]);
$more .= '<button type="button" class="post-drop-meta__item" data-aksrt-report="' . esc_attr((string) $report_payload) . '"><i class="fa fas fa-flag" aria-hidden="true"></i><span>' . esc_html__('举报', 'aksrt') . '</span></button>';
} elseif ($ban_item === '') {
$more .= '<a href="' . esc_url($login_url) . '" class="post-drop-meta__item"><i class="fa fas fa-flag" aria-hidden="true"></i><span>' . esc_html__('登录后举报', 'aksrt') . '</span></a>';
}
$more .= '</div></div>';
return '<div class="flex ac single-metabox"><div class="post-metas">' . $meta . '</div>' . $more . '</div>';
}
endif;
if (!function_exists('aksrt_get_post_status_badge')) :
/**
* Non-publish status badge (text badge, legacy-compatible classes).
*/
function aksrt_get_post_status_badge($post = null): string {
$post = get_post($post);
if (!($post instanceof WP_Post)) {
return '';
}
$map = [
'draft' => ['label' => __('草稿', 'aksrt'), 'class' => 'jb-yellow'],
'pending' => ['label' => __('待审核', 'aksrt'), 'class' => 'jb-red'],
'private' => ['label' => __('私密', 'aksrt'), 'class' => 'b-black opacity8'],
'future' => ['label' => __('定时发布', 'aksrt'), 'class' => 'c-blue'],
'trash' => ['label' => __('回收站', 'aksrt'), 'class' => 'jb-red'],
];
$status = $post->post_status;
if (!isset($map[$status])) {
return '';
}
return '<span class="badge img-badge right ' . esc_attr($map[$status]['class']) . '">'
. esc_html($map[$status]['label']) . '</span>';
}
endif;
if (!function_exists('aksrt_get_colored_term_tags')) :
/**
* Colored but-links for terms.
*
* @param WP_Term[] $terms Terms.
* @param string $class Base class.
* @param string $before Prefix HTML inside link.
* @param string[] $palette Color classes.
*/
function aksrt_get_colored_term_tags(array $terms, string $class = 'but ml6 radius', string $before = '', array $palette = []): string {
if ($palette === []) {
$palette = ['c-blue', 'c-yellow', 'c-green', 'c-purple', 'c-red'];
}
$html = '';
$i = 0;
foreach ($terms as $term) {
if (!($term instanceof WP_Term)) {
continue;
}
$color = $palette[$i % count($palette)];
$i++;
$url = get_term_link($term);
if (is_wp_error($url)) {
continue;
}
$html .= '<a class="' . esc_attr(trim($class . ' ' . $color)) . '" href="' . esc_url($url) . '">'
. $before . esc_html($term->name) . '</a>';
}
return $html;
}
endif;
if (!function_exists('aksrt_get_single_article_tags_html')) :
/**
* Categories → tags.
*/
function aksrt_get_single_article_tags_html(int $post_id): string {
$cats = get_the_category($post_id) ?: [];
$cat_html = aksrt_get_colored_term_tags(
$cats,
'but ml6 radius',
'<i class="fa fas fa-folder-open" aria-hidden="true"></i> ',
['c-blue', 'c-yellow', 'c-green', 'c-purple', 'c-red']
);
$tags_html = '';
$tags = get_the_tags($post_id) ?: [];
if (!empty($tags) && is_array($tags)) {
// Sort by count desc like legacy.
usort($tags, static function ($a, $b) {
return (int) $b->count <=> (int) $a->count;
});
foreach ($tags as $tag) {
if (!($tag instanceof WP_Term)) {
continue;
}
$tags_html .= '<a href="' . esc_url(get_tag_link($tag->term_id)) . '" class="but ml6 radius"># ' . esc_html($tag->name) . '</a>';
}
}
$top = $cat_html;
if ($top === '' && $tags_html === '') {
return '';
}
return '<div class="theme-box article-tags">' . $top . ($tags_html !== '' ? '<br>' . $tags_html : '') . '</div>';
}
endif;
if (!function_exists('aksrt_get_post_allow_view_data')) :
/**
* Category/post reading permission gate (legacy subset).
*
* Supported term meta keys (first match wins, walks parents):
* - term meta `post_cat_config` array with `allow_view` = signin|login|roles
* - term meta `aksrt_allow_view` string
* - post meta `_aksrt_allow_view` string (public|login|signin|roles)
*
* @return array{allow:bool,type:string,not_html:string,allow_reason:string,html:string}
*/
function aksrt_get_post_allow_view_data($post = null): array {
static $cache = [];
$data = [
'allow' => true,
'type' => '',
'not_html' => '',
'allow_reason' => '',
'html' => '',
];
$post = get_post($post);
if (!($post instanceof WP_Post) || $post->post_type !== 'post') {
return $data;
}
if (isset($cache[$post->ID])) {
return $cache[$post->ID];
}
if (current_user_can('manage_options')) {
$data['allow_reason'] = __('您是管理员,可查看所有内容', 'aksrt');
$cache[$post->ID] = $data;
return $data;
}
// Post-level override.
$post_allow = sanitize_key((string) get_post_meta($post->ID, '_aksrt_allow_view', true));
if ($post_allow !== '' && $post_allow !== 'public') {
$resolved = aksrt_resolve_allow_view_rule($post_allow, [], $post);
$cache[$post->ID] = $resolved;
return $resolved;
}
$cats = get_the_category($post->ID);
if (empty($cats)) {
$cache[$post->ID] = $data;
return $data;
}
foreach ($cats as $cat) {
if (!($cat instanceof WP_Term)) {
continue;
}
$rule = aksrt_get_term_allow_view_rule((int) $cat->term_id);
if ($rule['type'] === '' || $rule['type'] === 'public') {
continue;
}
$resolved = aksrt_resolve_allow_view_rule($rule['type'], $rule['roles'], $post);
if (!$resolved['allow']) {
$cache[$post->ID] = $resolved;
return $resolved;
}
}
$cache[$post->ID] = $data;
return $data;
}
endif;
if (!function_exists('aksrt_get_term_allow_view_rule')) :
/**
* Walk category ancestors for allow_view config.
*
* @return array{type:string,roles:array<string,mixed>}
*/
function aksrt_get_term_allow_view_rule(int $term_id): array {
static $term_cache = [];
if (isset($term_cache[$term_id])) {
return $term_cache[$term_id];
}
$empty = ['type' => '', 'roles' => []];
$chain = array_merge([$term_id], get_ancestors($term_id, 'category'));
foreach ($chain as $id) {
$id = (int) $id;
$config = get_term_meta($id, 'post_cat_config', true);
if (is_array($config) && !empty($config['allow_view'])) {
$rule = [
'type' => sanitize_key((string) $config['allow_view']),
'roles' => is_array($config['allow_view_roles'] ?? null) ? $config['allow_view_roles'] : [],
];
$term_cache[$term_id] = $rule;
return $rule;
}
$flat = sanitize_key((string) get_term_meta($id, 'aksrt_allow_view', true));
if ($flat !== '' && $flat !== 'public') {
$rule = ['type' => $flat, 'roles' => []];
$term_cache[$term_id] = $rule;
return $rule;
}
}
$term_cache[$term_id] = $empty;
return $empty;
}
endif;
if (!function_exists('aksrt_resolve_allow_view_rule')) :
/**
* @param array<string,mixed> $roles Role constraints for type=roles.
* @return array{allow:bool,type:string,not_html:string,allow_reason:string,html:string}
*/
function aksrt_resolve_allow_view_rule(string $type, array $roles, WP_Post $post): array {
$type = sanitize_key($type);
if (in_array($type, ['login', 'signin'], true)) {
$type = 'signin';
}
$data = [
'allow' => true,
'type' => $type,
'not_html' => '',
'allow_reason' => '',
'html' => '',
];
$user_id = get_current_user_id();
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(get_permalink($post));
if ($type === 'signin') {
$data['html'] = '<p class="separator muted-3-color em09">' . esc_html__('登录后可查看', 'aksrt') . '</p>';
if ($user_id <= 0) {
$data['allow'] = false;
$data['not_html'] = aksrt_get_allow_view_deny_shell(
__('内容已隐藏,请登录后查看', 'aksrt'),
'<div class="text-center em09 mt20"><p class="separator muted-3-color mb20">' . esc_html__('登录后继续查看', 'aksrt') . '</p>'
. '<p><a href="' . esc_url($login_url) . '" class="but jb-blue padding-lg"><i class="fa fas fa-fw fa-sign-in-alt" aria-hidden="true"></i>' . esc_html__('登录', 'aksrt') . '</a></p></div>'
);
} else {
$data['allow_reason'] = __('您已登录,可查看此内容', 'aksrt');
}
return $data;
}
if ($type === 'roles') {
$need_vip = isset($roles['vip']) ? (int) $roles['vip'] : 0;
$need_level = isset($roles['level']) ? (int) $roles['level'] : 0;
$need_auth = !empty($roles['auth']);
$badges = '';
if ($need_vip > 0) {
$badges .= '<span class="badg mm3">' . esc_html(sprintf(__('VIP%d 及以上', 'aksrt'), $need_vip)) . '</span>';
}
if ($need_level > 0) {
$badges .= '<span class="badg mm3">' . esc_html(sprintf(__('等级 %d 及以上', 'aksrt'), $need_level)) . '</span>';
}
if ($need_auth) {
$badges .= '<span class="badg mm3">' . esc_html__('认证用户', 'aksrt') . '</span>';
}
$data['html'] = '<div class="text-center em09"><p class="separator muted-3-color">' . esc_html__('以下用户组可查看', 'aksrt') . '</p>' . $badges . '</div>';
$ok = false;
if ($user_id > 0) {
if ($need_vip > 0 && function_exists('aksrt_get_user_vip_level') && aksrt_get_user_vip_level($user_id) >= $need_vip) {
$ok = true;
}
if (!$ok && $need_level > 0) {
$user_level = class_exists('\AKSRT\User\Level') ? \AKSRT\User\Level::get_level($user_id) : 0;
if ($user_level >= $need_level) {
$ok = true;
}
}
if (!$ok && $need_auth) {
$verify = class_exists('\\AKSRT\\User\\AuthApply') && \AKSRT\User\AuthApply::is_verified($user_id) ? 'verified' : '';
if (in_array($verify, ['verified', '1', 'yes'], true)) {
$ok = true;
}
}
// If no specific role constraints, any logged-in user passes.
if (!$ok && $need_vip <= 0 && $need_level <= 0 && !$need_auth) {
$ok = true;
}
}
if (!$ok) {
$data['allow'] = false;
$body = $data['html'];
if ($user_id <= 0) {
$body .= '<p class="text-center mt10"><a href="' . esc_url($login_url) . '" class="but jb-blue padding-lg">' . esc_html__('登录', 'aksrt') . '</a></p>';
}
$data['not_html'] = aksrt_get_allow_view_deny_shell(__('内容已隐藏', 'aksrt'), $body);
} else {
$data['allow_reason'] = __('您满足查看条件', 'aksrt');
}
return $data;
}
// Unknown type → allow.
$data['type'] = '';
return $data;
}
endif;
if (!function_exists('aksrt_get_allow_view_deny_shell')) :
function aksrt_get_allow_view_deny_shell(string $title, string $body_html): string {
return '<div class="aksrt-panel text-center allow-view-deny">'
. '<div class="box-body">'
. '<p class="em12 muted-2-color mb10"><i class="fa fas fa-lock fa-2x" aria-hidden="true"></i></p>'
. '<h3 class="em12">' . esc_html($title) . '</h3>'
. $body_html
. '</div></div>';
}
endif;
if (!function_exists('aksrt_single_content_allow_view')) :
/**
* the_content filter — hide body when category/post allow_view denies.
*/
function aksrt_single_content_allow_view($content) {
if (is_admin() || !is_singular('post') || !in_the_loop() || !is_main_query()) {
return $content;
}
$data = aksrt_get_post_allow_view_data(get_post());
if (!empty($data['allow'])) {
return $content;
}
return (string) ($data['not_html'] ?? '');
}
endif;
// Register gate once helpers load.
add_filter('the_content', 'aksrt_single_content_allow_view', 8);
if (!function_exists('aksrt_render_graphic_card_post')) :
/**
* legacy parity: style-3 图文卡(相关推荐 / 作者卡文章条共用)。
*
* @param WP_Post $post 文章对象。
* @param array<string,mixed> $args {
* @type string $class .graphic 附加类(mb10 / em09)。
* @type int $cut 悬停第二行标题截断宽度。
* }
*/
function aksrt_render_graphic_card_post(WP_Post $post, array $args = []): string {
$args = wp_parse_args($args, [
'class' => 'mb10',
'cut' => 45,
]);
$subtitle = class_exists('\AKSRT\Core\PostMetaBox')
? (string) \AKSRT\Core\PostMetaBox::get_meta_value($post->ID, 'subtitle', '')
: '';
$title = get_the_title($post) . $subtitle;
$media = aksrt_renderer_resolve_images($post->ID, aksrt_renderer_config(['type' => 'card']));
$img = (string) ($media['primary'] ?? '');
if ($img === '') {
$img = aksrt_get_lazy_thumb('lg');
}
$time = aksrt_time_ago((string) get_the_time('Y-m-d H:i:s', $post));
$views = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post->ID) : 0;
$info = '<item>' . esc_html($time) . '</item>'
. '<item class="pull-right">' . aksrt_renderer_svg_icon('view') . ' ' . esc_html(aksrt_cut_count($views)) . '</item>';
return aksrt_graphic_card([
'type' => 'style-3',
'class' => (string) $args['class'],
'img' => $img,
'alt' => $title,
'link' => [
'url' => get_permalink($post),
'target' => '',
],
'text1' => esc_html($title),
'text2' => esc_html(aksrt_str_cut($title, 0, (int) $args['cut'], '...')),
'text3' => $info,
'lazy' => true,
'height_scale' => 70,
]);
}
endif;