Files
wordpress-theme-aksrt/inc/core-hooks.php
T
kate522 f629a4f373 feat(icons): add the FA5 catalog, REST endpoint and unified icon helpers
Mirror the reference theme's universal picker on FA5: a generated full
catalog of the bundled FA5 Free 5.15.4 fonts (src/Icons/fa5-icons.php,
generated from the shipped CSS + SVG fonts), a zibsvg group parsed from
the runtime sprite so it cannot drift, and GET /aksrt/v1/icons/catalog.

Shared admin pieces: aksrt_icon_markup()/aksrt_sanitize_icon_value()
with an SVG allowlist, the picker field/modal assets, and one canonical
aksrt-font-awesome handle registered through aksrt_enqueue_fontawesome().
2026-09-06 12:59:03 +08:00

633 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
/**
* AKSRT Theme Core Hooks
*
* Central registration for the theme's WordPress hooks.
*
* @package AKSRT
* @version 3.2.0
*/
defined('ABSPATH') || exit;
// ============================================================
// 评论功能开关
// ============================================================
add_filter('comments_open', function ($open, $post_id) {
if (class_exists('\AKSRT\Settings\Settings')) {
$display = \AKSRT\Settings\Settings::get_comment_display_settings();
if (!empty($display['close_comments'])) {
return false;
}
}
return $open;
}, 10, 2);
add_filter('comment_text', function ($comment_text, $comment = null, $args = []) {
if (function_exists('aksrt_render_comment_content')) {
return aksrt_render_comment_content((string) $comment_text);
}
return $comment_text;
}, 20, 3);
// ============================================================
// 前端隐藏 WordPress Admin Bar
// ============================================================
add_filter('show_admin_bar', '__return_false');
// ============================================================
// SMTP 邮件配置注入
// ============================================================
add_filter('wp_mail_from', function ($from) {
if (!class_exists('\AKSRT\Settings\Settings')) return $from;
$mail = \AKSRT\Settings\Settings::get_smtp();
if (empty($mail['enabled'])) return $from;
$configured = sanitize_email((string) ($mail['from_address'] ?? ''));
return $configured !== '' ? $configured : $from;
});
add_filter('wp_mail_from_name', function ($from_name) {
if (!class_exists('\AKSRT\Settings\Settings')) return $from_name;
$mail = \AKSRT\Settings\Settings::get_smtp();
if (empty($mail['enabled'])) return $from_name;
$configured = trim((string) ($mail['from_name'] ?? ''));
return $configured !== '' ? $configured : $from_name;
});
add_action('phpmailer_init', function ($phpmailer) {
$mail = class_exists('\\AKSRT\\Settings\\Settings')
? \AKSRT\Settings\Settings::get_smtp()
: [];
if (empty($mail['enabled']) || empty($mail['host'])) return;
$phpmailer->isSMTP();
$phpmailer->Host = $mail['host'];
$phpmailer->Port = (int) ($mail['port'] ?? 465);
$phpmailer->SMTPAuth = !empty($mail['auth']) && !empty($mail['username']);
$phpmailer->Username = (string) ($mail['username'] ?? '');
$phpmailer->Password = (string) ($mail['password'] ?? '');
$phpmailer->SMTPSecure = (string) ($mail['encryption'] ?? 'ssl');
$phpmailer->Timeout = 10;
$phpmailer->Timelimit = 15;
});
// mail_title_prefix is applied by the canonical renderer and by third-party wp_mail callers.
add_filter('wp_mail', function ($atts) {
if (!class_exists('\AKSRT\Settings\Settings') || empty($atts['subject'])) return $atts;
$branding = \AKSRT\Settings\Settings::get_mail_branding();
$subject = (string) $atts['subject'];
$site = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES);
$subject = preg_replace('/^\[' . preg_quote($site, '/') . '\]\s*/u', '', $subject);
if ($branding['title_prefix'] !== '' && strpos($subject, $branding['title_prefix']) !== 0) {
$atts['subject'] = $branding['title_prefix'] . ' ' . $subject;
} else {
$atts['subject'] = $subject;
}
return $atts;
});
// ============================================================
// 分类 Term Meta 字段(封面图 + 推荐描述)
// ============================================================
/**
* 编辑分类页面 - 显示自定义字段
*/
function aksrt_category_meta_fields($term) {
$cover_id = (int) get_term_meta($term->term_id, 'aksrt_category_cover_id', true);
$cover_url = $cover_id > 0 ? wp_get_attachment_url($cover_id) : '';
$recommend_desc = (string) get_term_meta($term->term_id, 'aksrt_category_recommend_desc', true);
?>
<tr class="form-field">
<th scope="row"><label for="aksrt_category_cover_id"><?php esc_html_e('分类封面图', 'aksrt'); ?></label></th>
<td>
<div id="aksrt-cover-preview" style="margin-bottom:8px;">
<?php if ($cover_url) : ?>
<img src="<?php echo esc_url($cover_url); ?>" style="max-width:200px;height:auto;border-radius:6px;" alt="">
<?php endif; ?>
</div>
<input type="hidden" id="aksrt_category_cover_id" name="aksrt_category_cover_id" value="<?php echo esc_attr((string) $cover_id); ?>">
<button type="button" class="button" id="aksrt-pick-cover"><?php esc_html_e('选择图片', 'aksrt'); ?></button>
<button type="button" class="button" id="aksrt-remove-cover" <?php echo $cover_id > 0 ? '' : 'style="display:none;"'; ?>><?php esc_html_e('移除', 'aksrt'); ?></button>
<p class="description"><?php esc_html_e('分类推荐模块的封面图片。', 'aksrt'); ?></p>
</td>
</tr>
<tr class="form-field">
<th scope="row"><label for="aksrt_category_recommend_desc"><?php esc_html_e('推荐描述', 'aksrt'); ?></label></th>
<td>
<textarea id="aksrt_category_recommend_desc" name="aksrt_category_recommend_desc" rows="3" class="large-text"><?php echo esc_textarea($recommend_desc); ?></textarea>
<p class="description"><?php esc_html_e('分类推荐模块中显示的描述文字。留空则使用分类默认描述。', 'aksrt'); ?></p>
</td>
</tr>
<?php
}
/**
* 新增分类页面 - 显示自定义字段
*/
function aksrt_category_meta_fields_add() {
?>
<div class="form-field">
<label for="aksrt_category_cover_id"><?php esc_html_e('分类封面图', 'aksrt'); ?></label>
<div id="aksrt-cover-preview" style="margin-bottom:8px;"></div>
<input type="hidden" id="aksrt_category_cover_id" name="aksrt_category_cover_id" value="">
<button type="button" class="button" id="aksrt-pick-cover"><?php esc_html_e('选择图片', 'aksrt'); ?></button>
<button type="button" class="button" id="aksrt-remove-cover" style="display:none;"><?php esc_html_e('移除', 'aksrt'); ?></button>
<p class="description"><?php esc_html_e('分类推荐模块的封面图片。', 'aksrt'); ?></p>
</div>
<div class="form-field">
<label for="aksrt_category_recommend_desc"><?php esc_html_e('推荐描述', 'aksrt'); ?></label>
<textarea id="aksrt_category_recommend_desc" name="aksrt_category_recommend_desc" rows="3"></textarea>
<p class="description"><?php esc_html_e('分类推荐模块中显示的描述文字。留空则使用分类默认描述。', 'aksrt'); ?></p>
</div>
<?php
}
add_action('category_edit_form_fields', 'aksrt_category_meta_fields', 10, 1);
add_action('category_add_form_fields', 'aksrt_category_meta_fields_add', 10, 1);
/**
* 保存分类 term meta
*/
function aksrt_save_category_meta(int $term_id) {
if (!current_user_can('manage_categories')) return;
$cover_id = isset($_POST['aksrt_category_cover_id']) ? absint($_POST['aksrt_category_cover_id']) : 0;
$recommend_desc = isset($_POST['aksrt_category_recommend_desc']) ? sanitize_textarea_field(wp_unslash($_POST['aksrt_category_recommend_desc'])) : '';
if ($cover_id > 0) {
update_term_meta($term_id, 'aksrt_category_cover_id', $cover_id);
} else {
delete_term_meta($term_id, 'aksrt_category_cover_id');
}
if ($recommend_desc !== '') {
update_term_meta($term_id, 'aksrt_category_recommend_desc', $recommend_desc);
} else {
delete_term_meta($term_id, 'aksrt_category_recommend_desc');
}
if (class_exists('\AKSRT\Settings\Settings')) {
\AKSRT\Settings\Settings::flush_category_recommend_cache();
}
}
add_action('edited_category', 'aksrt_save_category_meta', 10, 1);
add_action('created_category', 'aksrt_save_category_meta', 10, 1);
/**
* 分类编辑页加载媒体库脚本(wp_enqueue_media 仅在 AKSRT 面板加载,此处补上)
*/
add_action('admin_enqueue_scripts', function (string $hook_suffix) {
if ($hook_suffix === 'edit-tags.php' || $hook_suffix === 'term.php') {
wp_enqueue_media();
}
});
/**
* 分类编辑页内联 JS - 媒体选择器
*/
function aksrt_category_meta_admin_js() {
$screen = get_current_screen();
if (!$screen || $screen->taxonomy !== 'category') return;
?>
<script>
jQuery(function($) {
var frame;
$(document).on('click', '#aksrt-pick-cover', function(e) {
e.preventDefault();
if (typeof wp === 'undefined' || !wp.media) return;
if (frame) { frame.open(); return; }
frame = wp.media({
title: '<?php echo esc_js(__('选择分类封面图', 'aksrt')); ?>',
button: { text: '<?php echo esc_js(__('使用此图片', 'aksrt')); ?>' },
multiple: false
});
frame.on('select', function() {
var attachment = frame.state().get('selection').first().toJSON();
$('#aksrt_category_cover_id').val(attachment.id);
$('#aksrt-cover-preview').html('<img src="' + attachment.url + '" style="max-width:200px;height:auto;border-radius:6px;" alt="">');
$('#aksrt-remove-cover').show();
});
frame.open();
});
$(document).on('click', '#aksrt-remove-cover', function(e) {
e.preventDefault();
$('#aksrt_category_cover_id').val('');
$('#aksrt-cover-preview').html('');
$(this).hide();
});
});
</script>
<?php
}
add_action('admin_footer', 'aksrt_category_meta_admin_js');
// ============================================================
// 资源列表:/resources 虚拟归档(post + _aksrt_is_resource)
// ============================================================
add_action('init', function () {
add_rewrite_rule('^resources/?$', 'index.php?aksrt_is_resource_archive=1', 'top');
add_rewrite_rule('^resources/page/([0-9]+)/?$', 'index.php?aksrt_is_resource_archive=1&paged=$matches[1]', 'top');
// 旧单篇路径 /resource/{slug} → 301 到 post 永久链接
add_rewrite_rule('^resource/([^/]+)/?$', 'index.php?aksrt_legacy_resource=$matches[1]', 'top');
});
add_filter('query_vars', function ($vars) {
$vars[] = 'aksrt_is_resource_archive';
$vars[] = 'aksrt_legacy_resource';
$vars[] = 'type';
$vars[] = 'trem';
return $vars;
});
add_action('template_redirect', function () {
$slug = get_query_var('aksrt_legacy_resource');
if (!$slug) {
return;
}
$page = get_page_by_path((string) $slug, OBJECT, 'post');
if ($page) {
wp_redirect(get_permalink($page->ID), 301);
exit;
}
});
add_action('pre_get_posts', function ($query) {
if (is_admin() || !$query->is_main_query()) {
return;
}
if ((bool) $query->get('aksrt_is_resource_archive')) {
$query->set('post_type', 'post');
$query->set('post_status', 'publish');
$orderby = isset($_GET['orderby']) ? sanitize_key((string) $_GET['orderby']) : 'date';
if ($orderby === 'downloads') {
$query->set('meta_key', \AKSRT\Core\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY);
$query->set('orderby', 'meta_value_num');
$query->set('order', 'DESC');
} elseif ($orderby === 'views') {
$query->set('meta_key', \AKSRT\Core\DomainModel::RESOURCE_VIEW_COUNT_KEY);
$query->set('orderby', 'meta_value_num');
$query->set('order', 'DESC');
}
$price_filter = isset($_GET['price']) ? sanitize_key((string) $_GET['price']) : '';
$price_min = isset($_GET['price_min']) && $_GET['price_min'] !== '' ? (float) $_GET['price_min'] : null;
$price_max = isset($_GET['price_max']) && $_GET['price_max'] !== '' ? (float) $_GET['price_max'] : null;
$meta_query = [
'relation' => 'AND',
[
'key' => \AKSRT\Core\DomainModel::RESOURCE_MARKER_KEY,
'value' => '1',
'compare' => '=',
],
];
if ($price_filter === 'free') {
$meta_query[] = [
'relation' => 'OR',
[
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
'value' => 0,
'compare' => '=',
'type' => 'NUMERIC',
],
[
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
'compare' => 'NOT EXISTS',
],
];
} elseif ($price_filter === 'paid') {
$meta_query[] = [
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
'value' => 0,
'compare' => '>',
'type' => 'NUMERIC',
];
}
if ($price_min !== null || $price_max !== null) {
if ($price_min !== null && $price_max !== null) {
$meta_query[] = [
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
'value' => [$price_min, $price_max],
'compare' => 'BETWEEN',
'type' => 'NUMERIC',
];
} elseif ($price_min !== null) {
$meta_query[] = [
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
'value' => $price_min,
'compare' => '>=',
'type' => 'NUMERIC',
];
} else {
$meta_query[] = [
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
'value' => $price_max,
'compare' => '<=',
'type' => 'NUMERIC',
];
}
}
$existing = $query->get('meta_query');
if (!is_array($existing)) {
$existing = [];
}
$query->set('meta_query', array_merge($existing, $meta_query));
$query->set('cat', 0);
$query->set('tag', '');
}
// 站内搜索:分类/标签名搜索(内容类型始终为 post)
if ($query->is_search() && empty($query->get('post_type'))) {
$search_type = sanitize_key((string) $query->get('type'));
$trem = absint($query->get('trem'));
if ($trem > 0) {
$query->set('cat', $trem);
}
if (in_array($search_type, ['category', 'tag'], true)) {
$taxonomy = $search_type === 'category' ? 'category' : 'post_tag';
$term_ids = get_terms([
'taxonomy' => $taxonomy,
'hide_empty' => false,
'fields' => 'ids',
'number' => 20,
'search' => (string) $query->get('s'),
]);
$query->set('s', '');
if (is_wp_error($term_ids) || empty($term_ids)) {
$query->set('post__in', [0]);
} else {
$query->set('tax_query', [[
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => array_map('absint', $term_ids),
]]);
}
}
}
});
add_filter('template_include', function ($template) {
if (!get_query_var('aksrt_is_resource_archive')) {
return $template;
}
$candidate = locate_template(['archive-resources.php']);
return $candidate ?: $template;
});
add_filter('document_title_parts', function ($parts) {
if (get_query_var('aksrt_is_resource_archive')) {
$parts['title'] = __('资源', 'aksrt');
}
return $parts;
});
// EnlighterJS 代码高亮 — 白名单 data-enlighter-* 属性
// ============================================================
add_filter('wp_kses_allowed_html', function ($allowed, $context) {
if ($context !== 'post') {
return $allowed;
}
// Allow data-enlighter-* attributes on <pre> and <code>
$enlighter_attrs = [
'data-enlighter-language' => true,
'data-enlighter-theme' => true,
'data-enlighter-group' => true,
'data-enlighter-title' => true,
'data-enlighter-linenumbers' => true,
'data-enlighter-highlight' => true,
'data-enlighter-lineoffset' => true,
];
if (isset($allowed['pre'])) {
$allowed['pre'] = array_merge($allowed['pre'], $enlighter_attrs);
}
if (isset($allowed['code'])) {
$allowed['code'] = array_merge($allowed['code'], $enlighter_attrs);
}
return $allowed;
}, 10, 2);
// ============================================================
// WP 后台文章列表增强列 — 浏览/点赞/收藏/下载
// ============================================================
add_filter('manage_post_posts_columns', function ($columns) {
$new_columns = [];
foreach ($columns as $key => $label) {
$new_columns[$key] = $label;
if ($key === 'title') {
$new_columns['aksrt_stats'] = __('互动数据', 'aksrt');
}
}
return $new_columns;
});
add_action('admin_enqueue_scripts', function ($hook) {
if ($hook !== 'edit.php') return;
aksrt_enqueue_fontawesome();
});
add_action('manage_post_posts_custom_column', function ($column, $post_id) {
if ($column !== 'aksrt_stats') return;
$views = (int) get_post_meta($post_id, 'views', true);
$likes = (int) get_post_meta($post_id, 'like', true);
$favorites = (int) get_post_meta($post_id, 'favorite', true);
$downloads = (int) get_post_meta($post_id, 'download_count', true);
$format = function (int $n): string {
if ($n >= 10000) return round($n / 10000, 1) . '万';
if ($n >= 1000) return round($n / 1000, 1) . 'k';
return (string) $n;
};
$parts = [];
$parts[] = '<span title="' . esc_attr__('浏览', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-eye" style="color:#8a909c;font-size:12px;"></i> ' . esc_html($format($views)) . '</span>';
$parts[] = '<span title="' . esc_attr__('点赞', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-thumbs-up" style="color:#1890ff;font-size:12px;"></i> ' . esc_html($format($likes)) . '</span>';
$parts[] = '<span title="' . esc_attr__('收藏', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-star" style="color:#f39c12;font-size:12px;"></i> ' . esc_html($format($favorites)) . '</span>';
if ($downloads > 0) {
$parts[] = '<span title="' . esc_attr__('下载', 'aksrt') . '"><i class="fa fas fa-download" style="color:#52c41a;font-size:12px;"></i> ' . esc_html($format($downloads)) . '</span>';
}
echo implode(' &middot; ', $parts);
}, 10, 2);
// Support sorting by views/likes/favorites in admin
add_action('pre_get_posts', function ($query) {
if (!is_admin() || !$query->is_main_query()) return;
$orderby = $query->get('orderby');
$allowed = ['views', 'like', 'favorite', 'download_count'];
if (in_array($orderby, $allowed, true)) {
$query->set('meta_key', $orderby);
$query->set('orderby', 'meta_value_num');
}
});
// ============================================================
// Gutenberg 自定义块注册
// ============================================================
// Register category server-side so it exists before JS registerBlockType runs.
// Keep untyped + defensive: some plugins pass unexpected shapes; typed params can 500 the editor.
add_filter('block_categories_all', static function ($categories, $context = null) {
if (!is_array($categories)) {
return $categories;
}
foreach ($categories as $category) {
$slug = '';
if (is_array($category)) {
$slug = (string) ($category['slug'] ?? '');
} elseif (is_object($category) && isset($category->slug)) {
$slug = (string) $category->slug;
}
if ($slug === 'aksrt-blocks') {
return $categories;
}
}
$categories[] = [
'slug' => 'aksrt-blocks',
'title' => 'AKSRT',
'icon' => null,
];
return $categories;
}, 10, 2);
add_action('enqueue_block_editor_assets', function () {
$base_uri = get_template_directory_uri();
$blocks_js = get_template_directory() . '/assets/js/gutenberg-blocks.js';
if (file_exists($blocks_js)) {
wp_enqueue_script(
'aksrt-gutenberg-blocks',
$base_uri . '/assets/js/gutenberg-blocks.js',
// wp-media-utils: MediaUpload for carousel block
['wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-hooks', 'wp-media-utils', 'wp-data'],
(string) filemtime($blocks_js),
true
);
}
$blocks_css = get_template_directory() . '/assets/css/gutenberg-blocks.css';
if (file_exists($blocks_css)) {
wp_enqueue_style(
'aksrt-gutenberg-blocks',
$base_uri . '/assets/css/gutenberg-blocks.css',
[],
(string) filemtime($blocks_css)
);
}
});
// Front-end styles for Gutenberg blocks
add_action('enqueue_block_assets', function () {
if (is_admin()) return; // Already loaded in editor
$posts = [];
$queried = get_queried_object();
if ($queried instanceof \WP_Post) {
$posts[$queried->ID] = $queried;
}
global $wp_query;
if ($wp_query instanceof \WP_Query) {
foreach ((array) $wp_query->posts as $post) {
if ($post instanceof \WP_Post) {
$posts[$post->ID] = $post;
}
}
}
$has_carousel = false;
foreach ($posts as $post) {
if (has_block('aksrt/carousel', $post)) {
$has_carousel = true;
break;
}
}
if (!$has_carousel) return;
$base_uri = get_template_directory_uri();
$blocks_css = get_template_directory() . '/assets/css/gutenberg-blocks.css';
if (file_exists($blocks_css)) {
wp_enqueue_style(
'aksrt-gutenberg-blocks-front',
$base_uri . '/assets/css/gutenberg-blocks.css',
[],
(string) filemtime($blocks_css)
);
}
});
// ============================================================
// Gutenberg Carousel Block 渲染回调
// ============================================================
add_action('init', function () {
if (!function_exists('register_block_type')) return;
register_block_type('aksrt/carousel', [
'render_callback' => function (array $attributes): string {
$images = $attributes['images'] ?? [];
if (empty($images)) return '';
$autoplay = !empty($attributes['autoplay']) ? 'true' : 'false';
$delay = absint($attributes['delay'] ?? 3000);
$effect = sanitize_text_field($attributes['effect'] ?? 'slide');
$slides = '';
foreach ($images as $img) {
if (empty($img['url'])) continue;
$alt = esc_attr($img['alt'] ?? '');
$url = esc_url($img['url']);
$slides .= '<div class="swiper-slide"><img' . aksrt_get_lazy_attr('lazy_sider', (string) $url, '', aksrt_get_lazy_thumb('lg')) . ' alt="' . $alt . '"></div>';
}
if (empty($slides)) return '';
return '<div class="aksrt-carousel-block swiper" data-autoplay="' . $autoplay . '" data-delay="' . $delay . '" data-effect="' . $effect . '">'
. '<div class="swiper-wrapper">' . $slides . '</div>'
. '<div class="swiper-pagination"></div>'
. '<div class="swiper-button-prev"></div><div class="swiper-button-next"></div>'
. '</div>';
},
]);
});
// ============================================================
// 付费内容系统:the_content 过滤器
// ============================================================
/**
* 未购买的图库/视频正文只保留公开预览;购买卡由 single 模板渲染。
*/
add_filter('the_content', 'aksrt_paid_content_filter', 20);