Files
wordpress-theme-aksrt/src/Settings/ThemeSearch.php
T
kate522andClaude Fable 5 57ff405691 refactor(color): unify color sanitizing and picking behind shared components
- Add AKSRT\Core\Color::sanitize($value, $allow_alpha) as the single PHP
  color sanitizer; replace sanitize_hex_color calls and per-class regexes
  (FloatButtons, FieldSchema, ThemeSearch, icon cards, TicketStatusService)
- SchemaProvider / ControllerBase sanitize `color` fields as colors;
  alpha kept only when the schema sets showAlpha
- Split SchemaProvider OAuth validation and option matching into sibling
  traits to stay under the 1000-line limit
- Add AksColorPicker.vue + utils/color.js mirroring the PHP rules; AksField,
  FloatBtnCommonFields and TicketStatusPanel use it
- Widget background color input now uses wpColorPicker
- Remove dead AksCompositeField dimensions/spacing/background branches and
  the unused Gutenberg ColorPicker var
- Add color-unification contract test; update harness requires and tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-28 20:47:41 +08:00

602 lines
24 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\Settings;
defined('ABSPATH') || exit;
class ThemeSearch {
public static function search_placeholder(): string {
$placeholder = trim((string) Settings::get_value('search_placeholder', ''));
return $placeholder !== '' ? $placeholder : __('开启精彩搜索', 'aksrt');
}
public static function search_history_enabled(): bool {
$settings = Settings::get_all();
return !array_key_exists('search_history', $settings) || !empty($settings['search_history']);
}
public static function hot_search_enabled(): bool {
$settings = Settings::get_all();
return !array_key_exists('search_popular_key', $settings) || !empty($settings['search_popular_key']);
}
public static function hot_search_keywords(): array {
$items = self::search_popular_sticky();
$counts = get_option('search_keywords', []);
if (is_array($counts)) {
$items = array_merge($items, array_keys($counts));
}
$items = array_map(
static function ($keyword): string {
return trim(sanitize_text_field((string) $keyword));
},
$items
);
$items = array_values(array_filter(array_unique($items), static function (string $keyword): bool {
return $keyword !== '';
}));
$limit = max(1, min(50, absint(Settings::get_value('search_popular_key_num', 20))));
return array_slice($items, 0, $limit);
}
public static function search_popular_title(): string {
$title = trim((string) Settings::get_value('search_popular_title', ''));
return $title !== '' ? $title : __('热门搜索', 'aksrt');
}
public static function search_popular_sticky(): array {
$raw = Settings::get_value('search_popular_sticky', '');
if (is_array($raw)) {
$items = $raw;
} else {
$items = preg_split('/[\r\n,,]+/', (string) $raw) ?: [];
}
$items = array_map(static fn($k) => trim(sanitize_text_field((string) $k)), $items);
return array_values(array_filter(array_unique($items), fn(string $k) => $k !== ''));
}
public static function search_posts_enabled(): bool {
return !empty(Settings::get_value('search_posts', false));
}
public static function search_no_page(): bool {
return !empty(Settings::get_value('search_no_page', false));
}
public static function search_exclude_cats(): array {
$raw = Settings::get_value('search_exclude_cats', '');
if (is_array($raw)) {
$items = $raw;
} else {
$items = preg_split('/[,,\s]+/', (string) $raw) ?: [];
}
$items = array_map(static fn($id) => absint($id), $items);
return array_values(array_filter(array_unique($items), fn(int $id) => $id > 0));
}
public static function search_cat_enabled(): bool {
return !empty(Settings::get_value('search_cat', false));
}
public static function search_cat_in(): string {
return trim((string) Settings::get_value('search_cat_in', ''));
}
public static function search_more_cat_obj(): array {
$raw = Settings::get_value('search_more_cat_obj', []);
$items = is_array($raw) ? $raw : (preg_split('/[,,\s]+/', (string) $raw) ?: []);
return array_values(array_filter(array_unique(array_map('absint', $items))));
}
public static function search_custom_key_enabled(): bool {
return !empty(Settings::get_value('search_custom_key_s', false));
}
public static function search_custom_key(): array {
$raw = Settings::get_value('search_custom_key', ['title', 'content', 'excerpt', 'category', 'post_tag']);
if (is_array($raw)) {
$items = $raw;
} else {
$items = preg_split('/[,,\s]+/', (string) $raw) ?: [];
}
$items = array_map(static fn($k) => trim(sanitize_key((string) $k)), $items);
$valid = ['title', 'content', 'excerpt', 'category', 'post_tag', 'coment', 'user'];
return array_values(array_intersect(array_filter(array_unique($items)), $valid));
}
public static function search_facets_enabled(): bool {
return !empty(Settings::get_value('search_facets', []));
}
public static function search_facets_order(): array {
$raw = Settings::get_value('search_facets', []);
$items = is_array($raw) ? array_keys($raw) : [];
$items = array_map(static fn($s) => trim(sanitize_key((string) $s)), $items);
return array_values(array_filter(array_unique($items), fn(string $s) => $s !== ''));
}
public static function get_meilisearch_config(): array {
$s = Settings::get_all();
$opt = is_array($s['search_ms_opt'] ?? null) ? $s['search_ms_opt'] : [];
$enabled = !empty($s['search_ms_s']);
$api_url = esc_url_raw(trim((string) ($opt['api_url'] ?? 'http://127.0.0.1:7700')));
$api_key = sanitize_text_field((string) ($opt['api_key'] ?? ''));
$index_prefix = sanitize_key((string) ($opt['index_prefix'] ?? 'aksrt'));
$synonyms_raw = $opt['synonyms'] ?? [];
$synonyms = [];
foreach ((array) $synonyms_raw as $row) {
if (!is_array($row)) {
continue;
}
$key = trim(sanitize_text_field((string) ($row['key'] ?? '')));
$values = array_values(array_filter(array_map('trim', explode(',', sanitize_text_field((string) ($row['val'] ?? ''))))));
if ($key !== '' && $values) {
$synonyms[$key] = $values;
}
}
return [
'enabled' => $enabled,
'api_url' => $api_url,
'api_key' => $api_key,
'index_prefix' => $index_prefix !== '' ? $index_prefix : 'aksrt',
'synonyms' => $synonyms,
'index_name' => ($index_prefix !== '' ? $index_prefix : 'aksrt') . '_posts',
];
}
public static function filter_posts_search(string $search, \WP_Query $query): string {
if (!$query->is_search() || !$query->get('s') || !self::search_custom_key_enabled()) {
return $search;
}
if (sanitize_key((string) $query->get('type')) === 'user') {
return ' AND 1=0';
}
$config = self::get_meilisearch_config();
if (!empty($config['enabled'])) {
return $search;
}
$keys = self::search_custom_key();
if (!$keys) {
return $search;
}
global $wpdb;
$terms = (array) ($query->query_vars['search_terms'] ?? []);
if (!$terms) {
$terms = [(string) $query->get('s')];
}
$groups = [];
$taxonomies = ['post_tag', 'category'];
foreach ($terms as $term) {
$like = (!empty($query->query_vars['exact']) ? '' : '%') . $wpdb->esc_like((string) $term) . (!empty($query->query_vars['exact']) ? '' : '%');
$parts = [];
foreach ($keys as $key) {
if ($key === 'title') $parts[] = $wpdb->prepare("{$wpdb->posts}.post_title LIKE %s", $like);
elseif ($key === 'content') $parts[] = $wpdb->prepare("{$wpdb->posts}.post_content LIKE %s", $like);
elseif ($key === 'excerpt') $parts[] = $wpdb->prepare("{$wpdb->posts}.post_excerpt LIKE %s", $like);
elseif (in_array($key, $taxonomies, true)) $parts[] = $wpdb->prepare('(aksrt_search_tt.taxonomy = %s AND aksrt_search_terms.name LIKE %s)', $key, $like);
elseif ($key === 'coment') $parts[] = $wpdb->prepare('aksrt_search_comments.comment_content LIKE %s', $like);
elseif ($key === 'user') $parts[] = $wpdb->prepare('(aksrt_search_users.user_nicename LIKE %s OR aksrt_search_users.display_name LIKE %s)', $like, $like);
}
if ($parts) {
$groups[] = '(' . implode(' OR ', $parts) . ')';
}
}
if (!$groups) {
return $search;
}
if (!is_user_logged_in()) {
$groups[] = "({$wpdb->posts}.post_password = '')";
}
return ' AND ' . implode(' AND ', $groups);
}
public static function filter_posts_search_join(string $join, \WP_Query $query): string {
if (!$query->is_search() || !$query->get('s') || !self::search_custom_key_enabled() || !empty(self::get_meilisearch_config()['enabled'])) {
return $join;
}
global $wpdb;
$keys = self::search_custom_key();
if (in_array('coment', $keys, true)) {
$join .= " LEFT JOIN {$wpdb->comments} AS aksrt_search_comments ON ({$wpdb->posts}.ID = aksrt_search_comments.comment_post_ID)";
}
if (in_array('user', $keys, true)) {
$join .= " LEFT JOIN {$wpdb->users} AS aksrt_search_users ON ({$wpdb->posts}.post_author = aksrt_search_users.ID)";
}
if (array_intersect(['post_tag', 'category'], $keys)) {
$join .= " LEFT JOIN {$wpdb->term_relationships} AS aksrt_search_tr ON ({$wpdb->posts}.ID = aksrt_search_tr.object_id)";
$join .= " LEFT JOIN {$wpdb->term_taxonomy} AS aksrt_search_tt ON (aksrt_search_tr.term_taxonomy_id = aksrt_search_tt.term_taxonomy_id)";
$join .= " LEFT JOIN {$wpdb->terms} AS aksrt_search_terms ON (aksrt_search_tt.term_id = aksrt_search_terms.term_id)";
}
return $join;
}
public static function filter_posts_search_distinct(string $distinct, \WP_Query $query): string {
return $query->is_search() && $query->get('s') && self::search_custom_key_enabled() ? 'DISTINCT' : $distinct;
}
public static function filter_search_query($query): void {
if (is_admin() || !$query->is_main_query() || !$query->is_search()) {
return;
}
$type = isset($_REQUEST['type']) ? sanitize_key((string) wp_unslash($_REQUEST['type'])) : '';
if (!in_array($type, ['post', 'user'], true)) {
$type = self::get_search_content_types()['default'];
}
$query->set('type', $type);
if ($type === 'user') {
$query->set('post__in', [0]);
return;
}
if (self::search_no_page()) {
$existing = $query->get('post_type');
if (empty($existing)) {
$query->set('post_type', 'post');
}
}
$exclude_cats = self::search_exclude_cats();
if (!empty($exclude_cats)) {
foreach ($exclude_cats as $category_id) {
$children = get_term_children($category_id, 'category');
if (!is_wp_error($children)) {
$exclude_cats = array_merge($exclude_cats, $children);
}
}
$query->set('category__not_in', array_values(array_unique(array_map('absint', $exclude_cats))));
}
$term_id = isset($_REQUEST['trem']) ? absint(wp_unslash($_REQUEST['trem'])) : 0;
if ($term_id > 0) {
$term = get_term($term_id);
if ($term instanceof \WP_Term) {
$tax_query = (array) $query->get('tax_query');
$tax_query[] = [
'taxonomy' => $term->taxonomy,
'field' => 'term_id',
'terms' => $term->term_id,
];
$query->set('tax_query', $tax_query);
}
}
$user_id = isset($_REQUEST['user']) ? absint(wp_unslash($_REQUEST['user'])) : 0;
if ($user_id > 0) {
$query->set('author', $user_id);
}
$orderby = isset($_REQUEST['orderby']) ? sanitize_key((string) wp_unslash($_REQUEST['orderby'])) : '';
$meta_keys = [
'views' => 'aksrt_post_views',
'like' => '_aksrt_like_count',
'favorite' => '_aksrt_favorite_count',
'sales_volume' => '_aksrt_sales_volume',
];
if (isset($meta_keys[$orderby])) {
$query->set('meta_key', $meta_keys[$orderby]);
$query->set('orderby', ['meta_value_num' => 'DESC', 'date' => 'DESC']);
$query->set('order', 'DESC');
} elseif ($orderby === 'comment_count') {
$query->set('orderby', 'comment_count');
$query->set('order', 'DESC');
} elseif ($orderby === 'date') {
$query->set('orderby', 'date');
$query->set('order', 'DESC');
}
}
public static function apply_list_orderby($query): void {
if (is_admin() || !$query->is_main_query()) {
return;
}
if (!$query->is_archive() && !$query->is_search() && !$query->is_home()) {
return;
}
if ($query->get('orderby')) {
return;
}
$s = Settings::get_all();
$key = sanitize_key((string) ($s['list_orderby'] ?? 'modified'));
if ($key === '' || $key === 'default') {
return;
}
switch ($key) {
case 'modified':
$query->set('orderby', 'modified');
$query->set('order', 'DESC');
break;
case 'date':
$query->set('orderby', 'date');
$query->set('order', 'DESC');
break;
case 'comments':
$query->set('orderby', 'comment_count');
$query->set('order', 'DESC');
break;
}
}
public static function get_search_content_types(): array {
$types = ['post', 'user'];
$default = sanitize_key((string) Settings::get_value('search_type_in', 'post'));
if (!in_array($default, $types, true)) {
$default = 'post';
}
return [
'types' => $types,
'default' => $default,
];
}
public static function theme_mode(): string {
$mode = sanitize_key((string) Settings::get_value('theme_mode', 'time-auto'));
$valid = ['white-theme', 'dark-theme', 'time-auto'];
return in_array($mode, $valid, true) ? $mode : 'time-auto';
}
public static function theme_mode_js_value(): string {
$mode = self::theme_mode();
$map = ['white-theme' => 'light', 'dark-theme' => 'dark', 'time-auto' => 'time-auto'];
return $map[$mode] ?? 'time-auto';
}
public static function time_auto_theme_mode(): string {
$hour = (int) current_time('G');
return ($hour >= 20 || $hour < 8) ? 'dark' : 'light';
}
public static function initial_theme_mode(): string {
$cookie = isset($_COOKIE['theme_mode'])
? sanitize_key((string) wp_unslash($_COOKIE['theme_mode']))
: '';
$map = [
'dark-theme' => 'dark',
'white-theme' => 'light',
'dark' => 'dark',
'light' => 'light',
];
if (isset($map[$cookie])) {
return $map[$cookie];
}
$mode = self::theme_mode();
if ($mode === 'dark-theme') {
return 'dark';
}
if ($mode === 'white-theme') {
return 'light';
}
if ($mode === 'time-auto') {
return self::time_auto_theme_mode();
}
return '';
}
/** 全局主题色:theme_skin_custom 优先,清空时使用 theme_skin 预置色(对齐参考主题 zib_head_css)。 */
public static function theme_color(): string {
$custom = \AKSRT\Core\Color::sanitize(Settings::get_value('theme_skin_custom', ''));
if ($custom !== '') {
return $custom;
}
$skin = ltrim(trim((string) Settings::get_value('theme_skin', 'f04494')), '#');
return preg_match('/^[0-9a-fA-F]{6}$/', $skin) ? '#' . $skin : '#f04494';
}
/** 夜间模式主题色;未设置返回空串,夜间沿用全局主题色。 */
public static function dark_theme_color(): string {
return \AKSRT\Core\Color::sanitize(Settings::get_value('dark_theme_skin_custom', ''));
}
public static function get_theme_mode_button_positions(): array {
$raw = Settings::get_value('theme_mode_button', ['pc_nav', 'm_menu']);
return array_values(array_intersect(array_map('sanitize_key', (array) $raw), ['pc_nav', 'm_menu']));
}
/** 卡片圆角 theme_main_radius(0–30,默认 12)→ --main-radius。 */
public static function main_radius(): int {
return max(0, min(30, (int) Settings::get_value('theme_main_radius', 12)));
}
public static function close_backdrop(): bool {
return !empty(Settings::get_value('close_backdrop', false));
}
/**
* 背景图 CSS 声明(不含选择器);无颜色且无图片时返回空串。
* 子键白名单与 GlobalSchema::background_field() / layout.js bgField 一致。
*/
public static function background_css(string $key): string {
$bg = Settings::get_value($key, []);
if (!is_array($bg)) {
return '';
}
$color = \AKSRT\Core\Color::sanitize($bg['color'] ?? '');
$image = esc_url_raw(trim((string) ($bg['image'] ?? '')));
if ($color === '' && $image === '') {
return '';
}
$allowed = [
'position' => ['left top', 'left center', 'left bottom', 'center top', 'center center', 'center bottom', 'right top', 'right center', 'right bottom'],
'repeat' => ['repeat', 'no-repeat', 'repeat-x', 'repeat-y'],
'attachment' => ['scroll', 'fixed'],
'size' => ['100%', 'cover', 'contain', 'auto'],
'origin' => ['padding-box', 'border-box', 'content-box'],
];
$css = $color !== '' ? 'background-color:' . $color . ';' : '';
if ($image !== '') {
$css .= 'background-image:url("' . str_replace(['"', '\\', "\n", "\r"], '', $image) . '");';
foreach ($allowed as $prop => $values) {
$value = (string) ($bg[$prop] ?? '');
if (in_array($value, $values, true)) {
$css .= 'background-' . $prop . ':' . $value . ';';
}
}
}
return $css;
}
public static function is_qj_loading_enabled(): bool {
return (bool) Settings::get_value('qj_loading', false);
}
public static function get_qj_loading_style(): string {
$style = (string) Settings::get_value('qj_dh_xs', 'no1');
if (!preg_match('/^no([1-9]|10)$/', $style)) {
return 'no1';
}
return $style;
}
public static function container_max_width(): int {
$width = (int) Settings::get_value('container_max_width', 1200);
return max(1200, min(2200, $width));
}
public static function font_family(): string {
$family = sanitize_key((string) Settings::get_value('font_family', 'system'));
return in_array($family, ['system', 'noto-sans-sc', 'noto-serif-sc', 'custom'], true) ? $family : 'system';
}
/** 自定义 font-family 字体栈,只保留字体名、引号、逗号等安全字符。 */
public static function font_family_custom(): string {
$stack = (string) Settings::get_value('font_family_custom', '');
$stack = preg_replace('/[^\p{L}\p{N}\s,"\'\-_.]/u', '', $stack);
return trim((string) preg_replace('/\s+/', ' ', (string) $stack), " ,");
}
public static function get_code_highlight_config(): array {
$s = Settings::get_all();
$light_themes = ['enlighter','bootstrap4','classic','beyond','mowtwo','eclipse','droide','minimal','rowhammer','godzilla','dracula','atomic','monokai'];
$dark_themes = ['dracula','atomic','monokai','enlighter','bootstrap4'];
$light = sanitize_key((string) ($s['highlight_zt'] ?? 'enlighter'));
$dark = sanitize_key((string) ($s['highlight_dark_zt'] ?? 'monokai'));
$max_height_raw = $s['highlight_maxheight'] ?? 400;
$max_height = absint(is_numeric($max_height_raw) ? $max_height_raw : preg_replace('/[^0-9]/', '', $max_height_raw));
if ($max_height === 0 && !in_array($max_height_raw, [0, '0', '0px'], true)) {
$max_height = 400;
}
return [
'enabled' => !isset($s['highlight_kg']) || !empty($s['highlight_kg']),
'lightTheme' => in_array($light, $light_themes, true) ? $light : 'enlighter',
'darkTheme' => in_array($dark, $dark_themes, true) ? $dark : 'monokai',
'lineNumbers' => !empty($s['highlight_hh']),
'showButtons' => !isset($s['highlight_btn']) || !empty($s['highlight_btn']),
'maxHeight' => $max_height,
];
}
public static function get_system_notice_config(): array {
$s = Settings::get_all();
$policy = sanitize_key((string) ($s['system_notice_policy'] ?? ''));
if (!in_array($policy, ['', 'signin', 'vip', 'free', 'auth'], true)) {
$legacy_map = ['always' => '', 'once' => '', 'daily' => ''];
$policy = $legacy_map[$policy] ?? '';
}
$size = sanitize_key((string) ($s['system_notice_size'] ?? ''));
if (!in_array($size, ['', 'modal-mini', 'modal-sm', 'modal-lg'], true)) {
$size = '';
}
$radius_raw = $s['system_notice_radius'] ?? '0';
$radius = absint(preg_replace('/[^0-9]/', '', $radius_raw));
$allowed_colors = ['primary', 'success', 'warning', 'danger', 'info', 'purple', 'dark'];
$buttons = [];
for ($i = 1; $i <= 4; $i++) {
$text = trim((string) ($s["system_notice_btn{$i}_text"] ?? ''));
$link = esc_url_raw((string) ($s["system_notice_btn{$i}_link"] ?? ''));
$color = sanitize_key((string) ($s["system_notice_btn{$i}_color"] ?? 'primary'));
if ($i === 1 && $text === '') {
$text = trim((string) ($s['system_notice_button'] ?? ''));
$link = $link ?: esc_url_raw((string) ($s['system_notice_link'] ?? ''));
}
if ($text === '') continue;
if (!in_array($color, $allowed_colors, true)) {
$color = 'primary';
}
$buttons[] = [
'text' => $text,
'link' => $link,
'color' => $color,
];
}
return [
'enabled' => !empty($s['system_notice_enabled']),
'policy' => $policy,
'title' => trim((string) ($s['system_notice_title'] ?? '')),
'content' => trim((string) ($s['system_notice_content'] ?? '')),
'buttons' => $buttons,
'size' => $size,
'radius' => $radius,
'class' => sanitize_html_class((string) ($s['system_notice_class'] ?? '')),
'title_icon' => trim((string) ($s['system_notice_title_icon'] ?? '')),
'title_class' => sanitize_html_class((string) ($s['system_notice_title_class'] ?? '')),
'title_style' => trim((string) ($s['system_notice_title_style'] ?? '')),
'expires' => absint($s['system_notice_expires'] ?? 24),
];
}
public static function get_sidebar_config(): array {
$s = Settings::get_all();
$position = sanitize_key((string) ($s['sidebar_position'] ?? 'right'));
$width = max(200, min(400, (int) ($s['sidebar_width'] ?? 311)));
return [
'enabled' => !isset($s['sidebar_enabled']) || !empty($s['sidebar_enabled']),
'position' => in_array($position, ['left', 'right'], true) ? $position : 'right',
'width' => $width,
'home' => !isset($s['sidebar_home_enabled']) || !empty($s['sidebar_home_enabled']),
'category' => !isset($s['sidebar_category_enabled']) || !empty($s['sidebar_category_enabled']),
'tag' => !isset($s['sidebar_tag_enabled']) || !empty($s['sidebar_tag_enabled']),
'single' => !isset($s['sidebar_single_enabled']) || !empty($s['sidebar_single_enabled']),
'page' => !isset($s['sidebar_page_enabled']) || !empty($s['sidebar_page_enabled']),
'search' => !isset($s['sidebar_search_enabled']) || !empty($s['sidebar_search_enabled']),
];
}
public static function get_i18n_config(): array {
$s = Settings::get_all();
$enabled = !empty($s['i18n_enabled']) || !empty($s['frontend_translate_enabled']);
$source = $s['i18n_default_lang'] ?? $s['frontend_translate_source'] ?? 'zh-CN';
return [
'enabled' => $enabled,
'autoDetect' => !isset($s['i18n_auto_detect']) || !empty($s['i18n_auto_detect']),
'defaultLang' => $source,
'availableLangs' => $s['i18n_available_langs'] ?? "zh-CN|简体中文\nen|English",
'nomenclature' => $s['i18n_nomenclature'] ?? '',
'switcherPosition' => $s['i18n_switcher_position'] ?? 'footer',
'urlStrategy' => $s['i18n_url_strategy'] ?? 'query',
];
}
}