Files
wordpress-theme-aksrt/src/Settings/UserRuntime.php
T
kate522andClaude Fable 5 b45fd161f0 fix(reports): separate readable categories from report content
Store readable category snapshots separately from details and evidence, validate report categories, and preserve context in admin displays and moderation actions. Widen report categories to TEXT and bump the schema to 3.2.0 without legacy data conversion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-31 00:57:13 +08:00

340 lines
13 KiB
PHP

<?php
/**
* @package AKSRT
*/
namespace AKSRT\Settings;
defined('ABSPATH') || exit;
class UserRuntime {
/**
* Canonical avatar, cover, and signature settings.
*
* @return array{avatar_upload_s:bool,avatar_external:string,avatar_default_img:string,user_cover_upload_s:bool,user_cover_img:string,user_desc_std:string}
*/
public static function get_user_feature_settings(): array {
$settings = Settings::read_domain('users', 'features');
$source = sanitize_key((string) ($settings['avatar_external'] ?? ''));
if (!in_array($source, ['', 'gravatar', 'weavatar', 'cravatar', 'geekzu', 'loli', 'sep', 'v2ex'], true)) {
$source = '';
}
$avatar = esc_url_raw((string) ($settings['avatar_default_img'] ?? ''));
$cover = esc_url_raw((string) ($settings['user_cover_img'] ?? ''));
return [
'avatar_upload_s' => !array_key_exists('avatar_upload_s', $settings) || !empty($settings['avatar_upload_s']),
'avatar_external' => $source,
'avatar_default_img' => $avatar !== '' ? $avatar : get_theme_file_uri('assets/img/avatar-default.png'),
'user_cover_upload_s' => !array_key_exists('user_cover_upload_s', $settings) || !empty($settings['user_cover_upload_s']),
'user_cover_img' => $cover !== '' ? $cover : get_theme_file_uri('assets/img/user_t.jpg'),
'user_desc_std' => sanitize_text_field((string) ($settings['user_desc_std'] ?? '这家伙很懒,什么都没有写...')),
];
}
/**
* Public user-home display settings.
*
* @return array
*/
public static function get_user_home_settings(): array {
$settings = Settings::read_domain('page_display', 'user-home');
$tab_options = is_array($settings['author_main_tab_opt'] ?? null)
? $settings['author_main_tab_opt']
: [
'post' => ['pc_s' => true, 'm_s' => true],
'favorite' => ['pc_s' => true, 'm_s' => true],
'comment' => ['pc_s' => true, 'm_s' => true],
'follow' => ['pc_s' => true, 'm_s' => true],
];
$map = ['post' => 'posts', 'favorite' => 'favorites', 'comment' => 'comments', 'follow' => 'follow'];
$ordered = [];
$pc_hide = [];
$mobile_hide = [];
foreach ($tab_options as $key => $option) {
$key = sanitize_key((string) $key);
if (!isset($map[$key]) || !is_array($option)) {
continue;
}
$tab = $map[$key];
$ordered[] = $tab;
if (empty($option['pc_s'])) {
$pc_hide[] = $tab;
}
if (empty($option['m_s'])) {
$mobile_hide[] = $tab;
}
}
return [
'route_identifier' => 'nicename',
'tabs' => $ordered,
'tabs_order' => $ordered,
'tabs_pc_hide' => $pc_hide,
'tabs_mobile_hide' => $mobile_hide,
'sidebar' => false,
'cover' => true,
'follow' => true,
'posts_per_page' => max(1, (int) get_option('posts_per_page', 10)),
'avatar' => true,
'name' => true,
'desc' => true,
'stats' => false,
'social' => true,
'tab_swiper' => !empty($settings['author_main_tab_swiper']),
'message' => true,
'private' => true,
'paging_ajax' => true,
];
}
/**
* Private user-center display settings.
*
* @return array{tabs:string[]}
*/
public static function get_user_center_settings(): array {
return [
'tabs' => ['profile', 'level', 'comments', 'credits', 'revenue', 'favorites', 'auth'],
];
}
/**
* User identity, level, medal, and report governance settings.
*
* @return array{
* verify:array{enabled:bool,public_display:bool,allow_apply:bool,disable_desc:string,desc:string,minimum_level:int},
* level:array{enabled:bool,public_display:bool,source:string,label_prefix:string,thresholds:array<int,int>},
* medal:array{enabled:bool,public_display:bool,limit:int,categories:array<int,array{cat_name:string,items:array<int,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int}>}>,items_by_slug:array<string,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int,cat_name:string}>},
* badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array<int,array{slug:string,label:string,tone:string,desc:string,icon_url?:string}>},
* report:array{enabled:bool,public_entry:bool,categories:array<int,array{slug:string,label:string}>}
* }
*/
public static function get_user_identity_settings(): array {
$settings = Settings::get_all();
$medal = self::parse_medal_catalog($settings);
return [
'verify' => [
'enabled' => !isset($settings['user_auth_s']) || !empty($settings['user_auth_s']),
'public_display' => !isset($settings['user_auth_s']) || !empty($settings['user_auth_s']),
'allow_apply' => !isset($settings['user_auth_apply_s']) || !empty($settings['user_auth_apply_s']),
'disable_desc' => (string) ($settings['user_auth_apply_disable_desc'] ?? ''),
'desc' => (string) ($settings['user_auth_apply_desc'] ?? ''),
'minimum_level' => max(0, min(30, (int) ($settings['user_auth_apply_limit_level'] ?? 0))),
],
'level' => [
'enabled' => !isset($settings['user_level_s']) || !empty($settings['user_level_s']),
'public_display' => !isset($settings['user_level_public_display']) || !empty($settings['user_level_public_display']),
'max' => max(3, min(30, (int) ($settings['user_level_max'] ?? 10))),
],
'medal' => $medal,
// Alias for existing call sites that still read badge.*; dictionary is derived from medal catalog.
'badge' => [
'enabled' => $medal['enabled'],
'public_display' => $medal['public_display'],
'limit' => $medal['limit'],
'dictionary' => array_values(array_map(static function (array $item): array {
return [
'slug' => $item['slug'],
'label' => $item['name'],
'tone' => 'info',
'desc' => (string) ($item['desc'] ?? ''),
'icon_url' => $item['icon'],
];
}, $medal['items_by_slug'])),
],
'report' => [
// 默认开启:对齐后台 schema default true 与参考主题 _pz('user_report_s', true)。
'enabled' => (bool) ($settings['user_report_s'] ?? $settings['user_report_enabled'] ?? true),
'public_entry' => !isset($settings['user_report_public_entry']) || !empty($settings['user_report_public_entry']),
],
];
}
/**
* 禁封/举报分区的运行时读取。
*
* get_all() 是裸 option:用户从未保存过“禁封/举报”分区时,
* ban_preset_reason / user_report_img_s / user_report_desc 等键不存在,
* schema 默认值只活在后台 UI 里。参考主题 _pz($key, $default) 的语义是
* 缺键即用默认值,因此这里改走 SchemaProvider 的带默认读取。
*/
private static function read_report_section(): array {
$settings = class_exists('\AKSRT\Admin\SchemaProvider')
? Settings::read_domain('users', 'report')
: [];
return is_array($settings) && $settings ? $settings : Settings::get_all();
}
/**
* 禁封设置
*/
public static function ban_settings(): array {
$settings = self::read_report_section();
$reasons_raw = $settings['ban_preset_reason'] ?? [];
$reasons = [];
foreach (is_array($reasons_raw) ? $reasons_raw : [] as $row) {
$text = is_array($row) ? sanitize_text_field((string) ($row['t'] ?? '')) : '';
if ($text !== '') {
$reasons[] = ['name' => $text];
}
}
// 申诉资料:group 数组 [{t:...}] 或旧版逗号分隔文本
$keys_raw = $settings['ban_appeal_keys'] ?? null;
$appeal_keys = [];
if (is_array($keys_raw)) {
foreach ($keys_raw as $row) {
$text = trim((string) ($row['t'] ?? ''));
if ($text !== '') {
$appeal_keys[] = sanitize_text_field($text);
}
}
} else {
$appeal_keys = array_filter(array_map('trim', explode(',', (string) $keys_raw)));
}
return [
'reasons' => $reasons,
'appeal_desc' => wp_kses_post((string) ($settings['ban_appeal_desc'] ?? '')),
'appeal_keys' => array_values($appeal_keys),
];
}
/**
* 举报设置
*/
public static function report_settings(): array {
$settings = self::read_report_section();
return [
// 默认开启:对齐后台 schema default true 与参考主题 _pz('user_report_s', true)。
'enabled' => (bool) ($settings['user_report_s'] ?? $settings['user_report_enabled'] ?? true),
'public_entry' => !isset($settings['user_report_public_entry']) || !empty($settings['user_report_public_entry']),
'img_enabled' => !empty($settings['user_report_img_s']),
'desc' => wp_kses_post((string) ($settings['user_report_desc'] ?? '')),
];
}
/**
* Parse structured user_medal_args catalog (rewrite; no badge dictionary dual-read).
*
* @param array<string,mixed> $settings
* @return array{enabled:bool,public_display:bool,limit:int,categories:array,items_by_slug:array}
*/
public static function parse_medal_catalog(array $settings): array {
$enabled = !empty($settings['user_medal_s']);
$public = !isset($settings['user_medal_public_display']) || !empty($settings['user_medal_public_display']);
$limit = max(1, min(8, (int) ($settings['user_medal_limit'] ?? 3)));
$allowed_types = [
'registration_time',
'checkin_all_day',
'new_post',
'comment',
'followed',
'like',
'comment_like',
'views',
'creator_income',
'affiliate_commission',
'manually_add',
];
$raw = $settings['user_medal_args'] ?? null;
if (is_string($raw) && $raw !== '') {
$decoded = json_decode($raw, true);
$raw = is_array($decoded) ? $decoded : [];
}
if (!is_array($raw)) {
$raw = [];
}
$categories = [];
$items_by_slug = [];
foreach ($raw as $cat) {
if (!is_array($cat)) {
continue;
}
$cat_name = sanitize_text_field((string) ($cat['cat_name'] ?? ''));
if ($cat_name === '') {
$cat_name = __('未命名', 'aksrt');
}
$items_out = [];
$items = is_array($cat['items'] ?? null) ? $cat['items'] : [];
foreach ($items as $item) {
if (!is_array($item)) {
continue;
}
$slug = sanitize_key((string) ($item['slug'] ?? ''));
$name = sanitize_text_field((string) ($item['name'] ?? ''));
if ($slug === '' || $name === '') {
continue;
}
$get_type = sanitize_key((string) ($item['get_type'] ?? 'manually_add'));
if (!in_array($get_type, $allowed_types, true)) {
$get_type = 'manually_add';
}
$icon = esc_url_raw((string) ($item['icon'] ?? ''));
$row = [
'slug' => $slug,
'name' => $name,
'desc' => sanitize_text_field((string) ($item['desc'] ?? '')),
'icon' => $icon,
'get_type' => $get_type,
'get_val' => max(0, (int) ($item['get_val'] ?? 0)),
'cat_name' => $cat_name,
];
$items_out[] = $row;
$items_by_slug[$slug] = $row;
}
$categories[] = [
'cat_name' => $cat_name,
'items' => $items_out,
];
}
return [
'enabled' => $enabled,
'public_display' => $public,
'limit' => $limit,
'categories' => $categories,
'items_by_slug' => $items_by_slug,
];
}
/**
* @return array<int,int>
*/
public static function parse_level_thresholds(string $raw): array {
$lines = preg_split('/[\r\n]+/', $raw, -1, PREG_SPLIT_NO_EMPTY);
$thresholds = [];
foreach ((array) $lines as $line) {
$parts = preg_split('/[|:,]/', trim($line), 2);
if (!is_array($parts) || count($parts) < 2) {
continue;
}
$level = (int) trim((string) $parts[0]);
$value = (int) trim((string) $parts[1]);
if ($level <= 0 || $value < 0) {
continue;
}
$thresholds[$level] = $value;
}
if (empty($thresholds)) {
$thresholds = [1 => 0, 2 => 30, 3 => 100, 4 => 300];
}
ksort($thresholds, SORT_NUMERIC);
return $thresholds;
}
}