feat(extensions): 新增系统工具页面与前台投稿分片上传

This commit is contained in:
2026-10-06 11:09:05 +08:00
parent 613b8a57a3
commit b75bf197a3
25 changed files with 1982 additions and 58 deletions
+5 -4
View File
@@ -14,10 +14,11 @@ class SchemaExtensions {
'domain' => 'extensions',
'title' => '扩展&增强',
'sections' => [
'ip' => SchemaExtensionsIP::build(),
'audit' => SchemaExtensionsAudit::build(),
'push' => SchemaExtensionsPush::build(),
'security' => SchemaExtensionsSecurity::build(),
'system' => SchemaExtensionsSystem::build(),
'ip' => SchemaExtensionsIP::build(),
'audit' => SchemaExtensionsAudit::build(),
'push' => SchemaExtensionsPush::build(),
'security' => SchemaExtensionsSecurity::build(),
],
];
}
@@ -0,0 +1,52 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaExtensionsSystem {
public static function build(): array {
return [
'key' => 'system',
'title' => '系统工具',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'optimize', 'title' => 'WordPress 优化', 'fields' => [
['key' => 'hide_admin_bar', 'label' => '前台关闭顶部admin_bar', 'type' => 'switch', 'default' => true, 'help' => '开启后前台不显示WordPress顶部黑条'],
['key' => 'remove_emoji', 'label' => '删除WordPress自带Emoji表情', 'type' => 'switch', 'default' => true],
['key' => 'remove_open_sans', 'label' => '禁用Google字体', 'type' => 'switch', 'default' => true],
['key' => 'remove_more_wp_head', 'label' => '清理多余的头部Meta标签', 'type' => 'switch', 'default' => true],
['key' => 'remove_dashboard_widgets', 'label' => 'WordPress 后台加速', 'type' => 'switch', 'default' => true, 'help' => '移除仪表盘的站点健康和WordPress活动等不必要功能,并禁止检测浏览器和PHP版本(这两个非常耗时),优化后台加载速度'],
['key' => 'display_wp_update', 'label' => '禁止WordPress检测更新', 'type' => 'switch', 'default' => true, 'help' => '开启后将禁用WordPress核心、插件和主题的自动更新及更新检测'],
]],
['key' => 'upload', 'title' => '上传优化', 'fields' => [
['key' => 'split_upload_s', 'label' => '大文件分片上传', 'type' => 'switch', 'default' => true, 'help' => '分片上传大文件,优化大文件上传效率(支持断点续传)'],
['key' => 'split_minimum_size', 'label' => '分片上传最小文件', 'type' => 'number', 'default' => 20, 'min' => 4, 'max' => 100, 'unit' => 'M', 'dependsOn' => ['key' => 'split_upload_s', 'value' => true], 'help' => '大于此大小的文件才会分片上传,请根据服务器性能合理设置;较大文件直接上传会比较耗时且无法突破系统上传大小限制,分片上传可以有效解决此问题;前端上传大小限制在「功能权限 → 上传权限」中设置。注意:能否顺利上传大文件还取决于服务器性能、带宽、CDN等因素,如果网站使用了OSS/COS等第三方云储存,则不建议在网站上传大文件'],
['key' => 'newfilename', 'label' => '上传文件自动重命名', 'type' => 'switch', 'default' => false, 'help' => '上传文件时系统会查询重名文件,开启此功能后可减少重名文件,从而提高上传效率'],
['key' => 'newfilename_type', 'label' => '重命名格式', 'type' => 'radio', 'default' => 'time', 'dependsOn' => ['key' => 'newfilename', 'value' => true], 'options' => [
['label' => '随机字串符', 'value' => 'random'],
['label' => '时间+原文件名', 'value' => 'time'],
]],
['key' => 'disabled_large_thumbnail', 'label' => '禁止WP大尺寸缩略图裁剪', 'type' => 'switch', 'default' => true, 'help' => '开启后将禁用WordPress自动裁剪medium_large、1536x1536、2048x2048的缩略图,可减少服务器硬盘压力,同时提高上传效率;如果您使用的插件必须要这些尺寸的缩略图,或使用了云储存的图片处理功能,则请关闭'],
]],
['key' => 'editor', 'title' => '编辑器与显示', 'fields' => [
['key' => 'admin_user_del_fields', 'label' => '关闭后台用户编辑多余的选项', 'type' => 'switch', 'default' => true, 'help' => '开启后在后台编辑用户资料时将不显示无用的多余选项'],
['key' => 'admin_frontend_set', 'label' => '前端页面编辑', 'type' => 'switch', 'default' => true, 'help' => '管理员可在前台快速修改页面、文章参数'],
['key' => 'close_gutenberg', 'label' => '禁用古腾堡编辑器', 'type' => 'switch', 'default' => false, 'help' => '后台文章编辑器将退回使用经典编辑器;禁用后主题的编辑器增强功能将不能使用,请酌情禁用!'],
['key' => 'disabled_autoembed', 'label' => '禁用Auto Embed', 'type' => 'switch', 'default' => true, 'help' => 'Auto Embed是WordPress自动检测并嵌入链接的一种功能,主要适配海外媒体网站,国内几乎用不上,建议禁用,可大幅度提高页面渲染速度'],
['key' => 'mce_float_toolbar', 'label' => '经典编辑器增强浮动工具栏', 'type' => 'checkbox', 'default' => ['pc_s', 'm_s'], 'sanitize_type' => 'array', 'options' => [
['label' => 'PC端开启', 'value' => 'pc_s'],
['label' => '移动端开启', 'value' => 'm_s'],
], 'help' => '开启后前端编辑器会显示浮动的工具栏,更加方便书写'],
['key' => 'time_ago_s', 'label' => '倒计时显示', 'type' => 'switch', 'default' => true, 'help' => '时间格式化为:X分钟前,X小时前,X天前....'],
['key' => 'time_format', 'label' => '自定义时间格式', 'type' => 'input', 'default' => 'n月j日 H:i', 'dependsOn' => ['key' => 'time_ago_s', 'operator' => 'falsy'], 'help' => '关闭倒计时显示后生效,时间格式接受标准PHP时间格式,请注意控制长度!'],
]],
],
];
}
}
-7
View File
@@ -131,13 +131,6 @@ class SchemaPermissions {
['key' => 'val', 'label' => '格式', 'type' => 'textarea', 'default' => '', 'rows' => 1, 'placeholder' => '如:svg=image/svg+xml,mp3=audio/mpeg'],
]],
]],
['key' => 'behavior', 'title' => '上传行为', 'fields' => [
['key' => 'newfilename', 'label' => '上传文件自动重命名', 'type' => 'switch', 'default' => false, 'help' => '开启后上传文件按规则重命名,减少重名文件查询,提高上传效率。'],
['key' => 'newfilename_type', 'label' => '重命名格式', 'type' => 'radio', 'default' => 'time', 'dependsOn' => ['key' => 'newfilename', 'value' => true], 'options' => [
['label' => '时间 + 原文件名', 'value' => 'time'],
['label' => '随机字符串', 'value' => 'random'],
]],
]],
],
],
'moderation' => [
+24 -7
View File
@@ -17,6 +17,7 @@ class FrontSubmit {
use FrontSubmitPay;
use FrontSubmitFeatured;
use FrontSubmitMedia;
use FrontSubmitChunk;
private static $instance = null;
@@ -93,6 +94,8 @@ class FrontSubmit {
],
],
]);
$this->register_chunk_routes();
}
public function can_upload_attachment(\WP_REST_Request $request): bool {
@@ -128,15 +131,26 @@ class FrontSubmit {
}
public function rest_upload_attachment(\WP_REST_Request $request) {
$kind = (string) $request->get_param('kind');
$user_id = get_current_user_id();
$files = $request->get_file_params();
$file = $files['file'] ?? null;
$kind = (string) $request->get_param('kind');
$files = $request->get_file_params();
$file = $files['file'] ?? null;
if (!is_array($file)) {
return new \WP_Error('aksrt_upload_missing_file', __('请选择要上传的文件。', 'aksrt'), ['status' => 400]);
}
return $this->persist_uploaded_file($kind, $file, false);
}
/**
* 校验并把一个上传文件落库为附件;整传与分片合并共用同一条管线。
*
* @param string $kind image|video|resource
* @param array $file $_FILES 风格数组(name/tmp_name/size/error)
* @param bool $sideload true 时文件不是 HTTP 直传(分片合并产物),用 sideload 动作保存
* @return \WP_REST_Response|\WP_Error
*/
public function persist_uploaded_file(string $kind, array $file, bool $sideload = false) {
$max_size = self::upload_limit_bytes($kind);
$upload_error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE);
if ($upload_error === UPLOAD_ERR_INI_SIZE || $upload_error === UPLOAD_ERR_FORM_SIZE) {
@@ -156,7 +170,7 @@ class FrontSubmit {
return new \WP_Error('aksrt_upload_failed', __('服务器无法保存上传文件。', 'aksrt'), ['status' => 500]);
}
$size = (int) ($file['size'] ?? 0);
$size = (int) ($file['size'] ?? 0);
if ($size <= 0 || $size > $max_size) {
return new \WP_Error(
@@ -197,15 +211,18 @@ class FrontSubmit {
require_once ABSPATH . 'wp-admin/includes/image.php';
}
// 合并产物不是 HTTP 直传文件,走 sideload 动作(copy 而非 move_uploaded_file)。
$overrides = $sideload ? ['test_form' => false, 'action' => 'wp_handle_sideload'] : ['test_form' => false];
$_FILES['aksrt_front_submit_file'] = $file;
$attachment_id = media_handle_upload('aksrt_front_submit_file', 0, [], ['test_form' => false]);
$attachment_id = media_handle_upload('aksrt_front_submit_file', 0, [], $overrides);
unset($_FILES['aksrt_front_submit_file']);
if (is_wp_error($attachment_id)) {
return new \WP_Error('aksrt_upload_failed', $attachment_id->get_error_message(), ['status' => 400]);
}
$attachment_id = (int) $attachment_id;
wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
wp_update_post(['ID' => $attachment_id, 'post_author' => get_current_user_id()]);
update_post_meta($attachment_id, '_aksrt_front_submit_media', $kind);
$attachment = $this->prepare_attachment_response($attachment_id);
+278
View File
@@ -0,0 +1,278 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
/**
* 前台大文件分片上传(后台「扩展&增强 → 系统工具」的 split_upload_s / split_minimum_size)。
*
* 行为对齐参考主题 action/media.php 的 user_split_upload / user_split_upload_merge /
* user_split_uploaded_chunk 协议:2MB 一片、断点续传(已传分片按序跳过)、合并产物
* 经 sideload 动作重新走 persist_uploaded_file() 标准校验与落库管线。
*/
trait FrontSubmitChunk {
/** 每个分片的固定大小(参考主题 split_chunk_size = 2MB)。 */
const CHUNK_SIZE_BYTES = 2 * 1048576;
public function register_chunk_routes(): void {
$kind_arg = [
'validate_callback' => static function ($value): bool {
return in_array((string) $value, ['image', 'video', 'resource'], true);
},
];
register_rest_route('aksrt/v1', '/front-submit/upload-chunk/(?P<kind>image|video|resource)', [
'methods' => 'POST',
'callback' => [$this, 'rest_upload_chunk'],
'permission_callback' => [$this, 'can_upload_attachment'],
'args' => [
'kind' => $kind_arg,
'chunk' => ['default' => 0, 'sanitize_callback' => 'absint'],
'chunks' => ['default' => 0, 'sanitize_callback' => 'absint'],
'name' => ['default' => '', 'sanitize_callback' => 'sanitize_file_name'],
'size' => ['default' => 0, 'sanitize_callback' => 'absint'],
],
]);
register_rest_route('aksrt/v1', '/front-submit/upload-merge/(?P<kind>image|video|resource)', [
'methods' => 'POST',
'callback' => [$this, 'rest_upload_chunk_merge'],
'permission_callback' => [$this, 'can_upload_attachment'],
'args' => [
'kind' => $kind_arg,
'chunks' => ['default' => 0, 'sanitize_callback' => 'absint'],
'name' => ['default' => '', 'sanitize_callback' => 'sanitize_file_name'],
'size' => ['default' => 0, 'sanitize_callback' => 'absint'],
],
]);
register_rest_route('aksrt/v1', '/front-submit/upload-status/(?P<kind>image|video|resource)', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [$this, 'rest_upload_chunk_status'],
'permission_callback' => [$this, 'can_upload_attachment'],
'args' => [
'kind' => $kind_arg,
'chunks' => ['default' => 0, 'sanitize_callback' => 'absint'],
'name' => ['default' => '', 'sanitize_callback' => 'sanitize_file_name'],
],
]);
}
/**
* 上传单个分片;index 为 0 基,同时累加校验总大小不超上限。
*/
public function rest_upload_chunk(\WP_REST_Request $request) {
$file = $request->get_file_params()['file'] ?? null;
if (!is_array($file) || empty($file['tmp_name']) || (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
return new \WP_Error('aksrt_chunk_missing_file', __('上传信息错误,请重新选择文件', 'aksrt'), ['status' => 400]);
}
$params = $this->chunk_params($request, true);
if (is_wp_error($params)) {
return $params;
}
$max_size = self::upload_limit_bytes($params['kind']);
if ($params['size'] <= 0 || $params['size'] > $max_size) {
return new \WP_Error('aksrt_upload_too_large', sprintf(__('文件大小不能超过 %s。', 'aksrt'), size_format($max_size)), ['status' => 413]);
}
$chunk_bytes = (int) @filesize((string) $file['tmp_name']);
if ($chunk_bytes <= 0 || $chunk_bytes > self::CHUNK_SIZE_BYTES) {
return new \WP_Error('aksrt_chunk_invalid_size', __('分片大小异常,请重新上传', 'aksrt'), ['status' => 400]);
}
$temp_dir = $this->chunk_temp_dir();
if ($params['chunk'] === 0) {
$this->chunk_temp_cleanup($temp_dir);
}
$existing = $this->chunk_part_paths($temp_dir, $params['key']);
$uploaded = 0;
foreach ($existing as $path) {
$uploaded += (int) @filesize($path);
}
if ($uploaded + $chunk_bytes > $max_size) {
return new \WP_Error('aksrt_upload_too_large', sprintf(__('文件大小不能超过 %s。', 'aksrt'), size_format($max_size)), ['status' => 413]);
}
$target = $temp_dir . '/' . $this->chunk_part_name($params['key'], $params['chunk']);
$moved = @move_uploaded_file((string) $file['tmp_name'], $target);
if (!$moved) {
$moved = @copy((string) $file['tmp_name'], $target);
@unlink((string) $file['tmp_name']);
}
if (!$moved) {
return new \WP_Error('aksrt_chunk_save_failed', __('分片保存失败,请重新上传', 'aksrt'), ['status' => 500]);
}
return new \WP_REST_Response([
'success' => true,
'chunk' => $params['chunk'],
'chunks' => $params['chunks'],
'uploaded_size' => $uploaded + $chunk_bytes,
], 200);
}
/**
* 断点续传:返回已落盘的分片序号,前端按序跳过。
*/
public function rest_upload_chunk_status(\WP_REST_Request $request) {
$params = $this->chunk_params($request);
if (is_wp_error($params)) {
return $params;
}
$parts = $this->chunk_part_paths($this->chunk_temp_dir(), $params['key']);
$uploaded = [];
foreach ($parts as $path) {
$uploaded[] = $this->chunk_index_from_name((string) $path);
}
return new \WP_REST_Response([
'success' => true,
'uploaded' => $uploaded,
'uploaded_count' => count($uploaded),
], 200);
}
/**
* 合并分片并按标准管线落库;成功或失败后都清理分片。
*/
public function rest_upload_chunk_merge(\WP_REST_Request $request) {
$params = $this->chunk_params($request);
if (is_wp_error($params)) {
return $params;
}
$max_size = self::upload_limit_bytes($params['kind']);
$temp_dir = $this->chunk_temp_dir();
$merge_path = $temp_dir . '/' . $this->chunk_part_name($params['key'], 'merge');
$fp = @fopen($merge_path, 'wb');
if (!$fp) {
return new \WP_Error('aksrt_chunk_merge_failed', __('分片合并失败,请重新上传', 'aksrt'), ['status' => 500]);
}
$missing = 0;
for ($i = 0; $i < $params['chunks']; $i++) {
$part = $temp_dir . '/' . $this->chunk_part_name($params['key'], $i);
if (!is_file($part)) {
$missing++;
break;
}
fwrite($fp, (string) file_get_contents($part));
}
fclose($fp);
if ($missing) {
@unlink($merge_path);
return new \WP_Error('aksrt_chunk_missing', __('文件缺少分片,正在重新上传缺失分片', 'aksrt'), ['status' => 409]);
}
$merged_size = (int) @filesize($merge_path);
if ($merged_size <= 0 || ($params['size'] > 0 && $merged_size !== $params['size'])) {
@unlink($merge_path);
return new \WP_Error('aksrt_chunk_size_mismatch', __('合并后的文件大小校验失败,请重新上传', 'aksrt'), ['status' => 400]);
}
if ($merged_size > $max_size) {
@unlink($merge_path);
return new \WP_Error('aksrt_upload_too_large', sprintf(__('文件大小不能超过 %s。', 'aksrt'), size_format($max_size)), ['status' => 413]);
}
$name = sanitize_file_name($params['name']);
$type = (string) (wp_check_filetype_and_ext($merge_path, $name, get_allowed_mime_types())['type'] ?? '');
$result = $this->persist_uploaded_file($params['kind'], [
'tmp_name' => $merge_path,
'name' => $name,
'size' => $merged_size,
'type' => $type,
'error' => UPLOAD_ERR_OK,
], true);
// 无论落库成败都清理本次分片(persist 成功时已 copy 走合并文件,失败时也要清场)。
foreach ($this->chunk_part_paths($temp_dir, $params['key']) as $path) {
@unlink($path);
}
@unlink($merge_path);
return $result;
}
/** 校验并归一分片参数(name/chunks/chunk/size/key)。 */
private function chunk_params(\WP_REST_Request $request, bool $with_index = false) {
$name = sanitize_file_name((string) $request->get_param('name'));
$chunks = (int) $request->get_param('chunks');
$chunk = (int) $request->get_param('chunk');
if ($name === '' || $chunks < 1 || $chunks > 20480) {
return new \WP_Error('aksrt_chunk_invalid_params', __('分片参数异常', 'aksrt'), ['status' => 400]);
}
if ($with_index && ($chunk < 0 || $chunk >= $chunks)) {
return new \WP_Error('aksrt_chunk_invalid_params', __('分片参数异常', 'aksrt'), ['status' => 400]);
}
return [
'kind' => (string) $request->get_param('kind'),
'name' => $name,
'chunks' => $chunks,
'chunk' => $chunk,
'size' => (int) $request->get_param('size'),
// 分片按用户隔离,避免同名文件互相覆盖、也防止探测他人分片。
'key' => md5(get_current_blog_id() . '|' . get_current_user_id() . '|' . $name),
];
}
/** 分片临时目录(参考主题为 wp-content/zib-temp,带 index.php 保护和 48h 清理)。 */
private function chunk_temp_dir(): string {
$temp_dir = (string) apply_filters('aksrt_temp_dir', WP_CONTENT_DIR . '/aksrt-temp');
if (!@is_dir($temp_dir)) {
wp_mkdir_p($temp_dir);
}
$index_file = $temp_dir . '/index.php';
if (!file_exists($index_file)) {
@file_put_contents($index_file, "<?php\n// Silence is golden.\n");
}
return $temp_dir;
}
/** 清理超过 48 小时的遗留分片。 */
private function chunk_temp_cleanup(string $temp_dir): void {
$expire = time() - 48 * HOUR_IN_SECONDS;
foreach ((array) glob($temp_dir . '/*.part') as $file) {
if (@filemtime((string) $file) < $expire) {
@unlink((string) $file);
}
}
}
/** 当前 key 的全部分片路径(不含 merge 产物)。 */
private function chunk_part_paths(string $temp_dir, string $key): array {
$paths = [];
foreach ((array) glob($temp_dir . '/' . $this->chunk_part_name($key, '*')) as $path) {
$path = (string) $path;
if (substr($path, -10) === 'merge.part') {
continue;
}
$paths[] = $path;
}
sort($paths, SORT_NATURAL);
return $paths;
}
private function chunk_part_name(string $key, $index): string {
return sprintf('%d-%s-%s.part', get_current_blog_id(), $key, (string) $index);
}
private function chunk_index_from_name(string $path): int {
$base = basename($path, '.part');
$dash = strrpos($base, '-');
return $dash === false ? -1 : (int) substr($base, $dash + 1);
}
}
-2
View File
@@ -213,8 +213,6 @@ trait ScriptsPart1c {
'upload_ext' => implode('|', array_keys(get_allowed_mime_types())),
'user_upload_nonce' => wp_create_nonce('user_upload'),
'post_action_nonce' => wp_create_nonce('post_action'),
'is_split_upload' => false,
'split_minimum_size' => 10,
'translate_config' => [],
'i18n' => [
'load_more' => __('加载更多', 'aksrt'),
+205
View File
@@ -0,0 +1,205 @@
<?php
/**
* 系统工具:WordPress 优化、上传优化与编辑器显示开关。
*
* 对应后台「扩展&增强 → 系统工具」设置(ExtensionRuntime::get_system_tools_settings()),
* 行为对齐参考主题 zib-theme.php / zib-head.php / admin-main.php / admin-set.php 的系统工具套件。
*
* @package AKSRT
*/
namespace AKSRT\Extensions;
defined('ABSPATH') || exit;
class SystemTools {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
$settings = \AKSRT\Settings\ExtensionRuntime::get_system_tools_settings();
if (!empty($settings['hide_admin_bar']) || !is_super_admin()) {
add_filter('show_admin_bar', '__return_false');
}
if (!empty($settings['remove_emoji'])) {
$this->remove_emoji();
}
if (!empty($settings['remove_open_sans'])) {
add_action('init', [$this, 'remove_open_sans']);
}
if (!empty($settings['remove_more_wp_head'])) {
add_action('after_setup_theme', [$this, 'remove_more_wp_head']);
}
if (!empty($settings['remove_dashboard_widgets'])) {
add_action('do_meta_boxes', [$this, 'remove_dashboard_widgets'], 10, 3);
add_filter('site_status_tests', [$this, 'filter_site_status_tests']);
add_filter('pre_http_request', [$this, 'block_site_health_requests'], 10, 3);
}
if (!empty($settings['display_wp_update'])) {
$this->disable_wp_update();
}
if (!empty($settings['disabled_large_thumbnail'])) {
add_action('after_setup_theme', [$this, 'disable_large_thumbnail']);
}
if (!empty($settings['close_gutenberg'])) {
add_filter('use_block_editor_for_post', '__return_false');
}
if (!empty($settings['disabled_autoembed'])) {
$this->disable_autoembed();
}
if (!empty($settings['admin_user_del_fields'])) {
add_action('show_user_profile', [$this, 'hide_user_profile_fields']);
add_action('edit_user_profile', [$this, 'hide_user_profile_fields']);
}
}
/** 删除WordPress自带Emoji表情(对齐参考主题 remove_emoji 套件)。 */
private function remove_emoji(): void {
remove_action('admin_print_scripts', 'print_emoji_detection_script');
remove_action('admin_print_styles', 'print_emoji_styles');
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');
remove_filter('the_content_feed', 'wp_staticize_emoji');
remove_filter('comment_text_rss', 'wp_staticize_emoji');
remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
}
/** 禁用Google字体(Open Sans)。 */
public function remove_open_sans(): void {
wp_deregister_style('open-sans');
wp_register_style('open-sans', false);
wp_enqueue_style('open-sans', '');
}
/** 清理wp_head带入的多余Meta标签。 */
public function remove_more_wp_head(): void {
remove_action('wp_head', 'feed_links_extra', 3);
remove_action('wp_head', 'feed_links', 2);
remove_action('wp_head', 'rsd_link');
remove_action('wp_head', 'wlwmanifest_link');
remove_action('wp_head', 'index_rel_link');
remove_action('wp_head', 'parent_post_rel_link', 10, 0);
remove_action('wp_head', 'start_post_rel_link', 10, 0);
remove_action('wp_head', 'adjacent_posts_rel_link', 10, 0);
remove_action('wp_head', 'wp_generator');
remove_action('wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0);
remove_action('wp_head', 'wp_shortlink_wp_head', 10, 0);
remove_action('wp_head', 'rest_output_link_wp_head', 10, 0);
remove_action('wp_head', 'wp_oembed_add_discovery_links', 10, 1);
remove_action('wp_head', 'rel_canonical', 10, 0);
}
/** 移除仪表盘的站点健康、PHP/浏览器版本检查小组件。 */
public function remove_dashboard_widgets($screen_id, $context): void {
if ('dashboard' !== $screen_id || 'normal' !== $context) {
return;
}
global $wp_meta_boxes;
unset($wp_meta_boxes['dashboard']['normal']['high']['dashboard_php_nag']);
unset($wp_meta_boxes['dashboard']['normal']['core']['dashboard_site_health']);
unset($wp_meta_boxes['dashboard']['side']['core']['dashboard_primary']);
}
/** 站点健康检查中移除页面缓存与PHP版本检测(非常耗时)。 */
public function filter_site_status_tests(array $tests): array {
if (isset($tests['async']['page_cache'])) {
unset($tests['async']['page_cache']);
}
if (isset($tests['direct']['php_version'])) {
unset($tests['direct']['php_version']);
}
return $tests;
}
/** 禁止后台联网检测PHP版本、浏览器与贡献者信息,提高后台加载速度。 */
public function block_site_health_requests($pre, $parsed_args, $url) {
$blocked = [
'api.wordpress.org/core/serve-happy/',
'api.wordpress.org/core/browse-happy/',
'api.wordpress.org/core/credits/',
];
foreach ($blocked as $arg) {
if (strpos((string) $url, $arg) !== false) {
return [
'headers' => [],
'body' => '',
'response' => [
'code' => false,
'message' => false,
],
'cookies' => [],
'http_response' => null,
];
}
}
return $pre;
}
/** 禁止WordPress检测核心/插件/主题更新,并屏蔽更新通知。 */
private function disable_wp_update(): void {
remove_action('admin_init', '_maybe_update_core');
remove_action('admin_init', '_maybe_update_plugins');
remove_action('admin_init', '_maybe_update_themes');
add_action('admin_menu', [$this, 'hide_update_notices']);
add_filter('automatic_updater_disabled', '__return_true');
remove_action('init', 'wp_schedule_update_checks');
remove_action('wp_version_check', 'wp_version_check');
remove_action('wp_update_plugins', 'wp_update_plugins');
remove_action('wp_update_themes', 'wp_update_themes');
add_filter('auto_update_core', '__return_false');
add_filter('allow_major_auto_core_updates', '__return_false');
add_filter('allow_dev_auto_core_updates', '__return_false');
add_filter('allow_minor_auto_core_updates', '__return_false');
}
/** 禁用更新通知。 */
public function hide_update_notices(): void {
remove_action('admin_notices', 'update_nag', 3);
remove_action('network_admin_notices', 'update_nag', 3);
}
/** 禁止WP大尺寸缩略图裁剪(medium_large、1536x1536、2048x2048)。 */
public function disable_large_thumbnail(): void {
remove_image_size('1536x1536');
remove_image_size('2048x2048');
add_filter('intermediate_image_sizes', static function ($sizes) {
return array_merge(array_diff((array) $sizes, ['medium_large', '1536x1536', '2048x2048']));
}, 20);
}
/** 禁用Auto Embed自动嵌入功能,提高页面渲染速度。 */
private function disable_autoembed(): void {
remove_filter('the_content', [$GLOBALS['wp_embed'], 'autoembed'], 8);
remove_filter('widget_text_content', [$GLOBALS['wp_embed'], 'autoembed'], 8);
remove_filter('widget_block_content', [$GLOBALS['wp_embed'], 'autoembed'], 8);
remove_action('edit_form_advanced', [$GLOBALS['wp_embed'], 'maybe_run_ajax_cache']);
remove_action('edit_page_form', [$GLOBALS['wp_embed'], 'maybe_run_ajax_cache']);
}
/** 关闭后台用户编辑页面中的多余选项。 */
public function hide_user_profile_fields(): void {
echo '<style>.user-first-name-wrap,.user-last-name-wrap,.user-admin-bar-front-wrap,.user-comment-shortcuts-wrap,.user-syntax-highlighting-wrap,.user-rich-editing-wrap,.user-profile-picture .description,.user-language-wrap{display:none}</style>';
}
}
+305
View File
@@ -0,0 +1,305 @@
<?php
/**
* 前端页面编辑(后台「扩展&增强 → 系统工具」的 admin_frontend_set)。
*
* 对齐参考主题 zib-frontend-set.php:管理员在前台通过悬浮按钮打开弹窗,
* 快速修改当前文章/页面的标题、副标题、布局、计数、评论开关或分类术语的
* 名称与描述;保存走白名单,行为与后台 PostMetaBox / wp_update_term 一致。
*
* @package AKSRT
*/
namespace AKSRT\Frontend;
defined('ABSPATH') || exit;
class PageAdminSet {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_filter('aksrt_float_right_buttons', [$this, 'float_button'], 10, 2);
add_action('wp_ajax_aksrt_frontend_set_modal', [$this, 'ajax_modal']);
add_action('wp_ajax_aksrt_frontend_set_save', [$this, 'ajax_save']);
add_action('wp_enqueue_scripts', [$this, 'enqueue_script']);
}
/** 开启条件:设置开关 + 管理员 + 单页/术语页 + 有效对象(排除投稿与导航页模板)。 */
public static function enabled(): bool {
if (!class_exists('\AKSRT\Settings\ExtensionRuntime') || !is_user_logged_in()) {
return false;
}
$settings = \AKSRT\Settings\ExtensionRuntime::get_system_tools_settings();
if (empty($settings['admin_frontend_set']) || !current_user_can('manage_options')) {
return false;
}
if (!get_queried_object_id() || (!is_single() && !is_page() && !is_category() && !is_tag() && !is_tax())) {
return false;
}
if (is_page_template('templates/pages/page-submit.php') || is_page_template('templates/pages/page-posts-navs.php')) {
return false;
}
return true;
}
/** 悬浮按钮:注入右侧悬浮按钮栏(对齐参考主题 zib_admin_frontend_set_botton)。 */
public function float_button($buttons, $is_mobile) {
if (!self::enabled()) {
return $buttons;
}
$url = add_query_arg(
[
'action' => 'aksrt_frontend_set_modal',
'_wpnonce' => wp_create_nonce('aksrt_frontend_set'),
],
admin_url('admin-ajax.php')
);
$button = '<a class="float-btn admin-set-btn" href="javascript:;"'
. ' data-aksrt-modal="remote" data-aksrt-modal-cache="false" data-aksrt-modal-kind="admin-set"'
. ' data-aksrt-modal-url="' . esc_url($url) . '"'
. aksrt_float_btn_tooltip_attr(__('在此编辑此页面参数', 'aksrt'), (bool) $is_mobile)
. '><i class="fa fas fa-cog"></i></a>';
return $button . (string) $buttons;
}
/** 条件加载弹窗提交脚本。 */
public function enqueue_script(): void {
if (!self::enabled()) {
return;
}
$src = get_template_directory() . '/assets/js/page-admin-set.js';
wp_enqueue_script(
'aksrt-page-admin-set',
get_template_directory_uri() . '/assets/js/page-admin-set.js',
[],
file_exists($src) ? (string) filemtime($src) : null,
true
);
}
/** GET 弹窗内容(注入 aksrt-modal-runtime)。 */
public function ajax_modal(): void {
check_ajax_referer('aksrt_frontend_set', '_wpnonce');
if (!self::enabled()) {
wp_send_json_error(['msg' => __('权限不足', 'aksrt')]);
}
$object_id = (int) get_queried_object_id();
if (is_category() || is_tag() || is_tax()) {
$html = $this->term_modal_html($object_id, (string) get_queried_object()->taxonomy);
} else {
$html = $this->post_modal_html($object_id, get_post_type($object_id) ?: 'post');
}
wp_send_json_success(['html' => $html]);
}
/** POST 保存:仅接受白名单字段。 */
public function ajax_save(): void {
check_ajax_referer('aksrt_frontend_set', '_wpnonce');
if (!self::enabled()) {
wp_send_json_error(['msg' => __('权限不足', 'aksrt')]);
}
$object_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
if (!$object_id) {
wp_send_json_error(['msg' => __('页面数据出错', 'aksrt')]);
}
$type = isset($_POST['type']) ? sanitize_key((string) $_POST['type']) : '';
if ($type === 'tax') {
$taxonomy = isset($_POST['subtype']) ? sanitize_key((string) $_POST['subtype']) : '';
$this->save_term($object_id, $taxonomy);
} else {
$this->save_post($object_id);
}
wp_send_json_success(['msg' => __('保存成功,正在刷新页面', 'aksrt'), 'reload' => true]);
}
private function save_post(int $post_id): void {
$post = get_post($post_id);
if (!$post) {
wp_send_json_error(['msg' => __('未找到可用数据', 'aksrt')]);
}
if (!current_user_can('edit_post', $post_id)) {
wp_send_json_error(['msg' => __('权限不足', 'aksrt')]);
}
$postarr = ['ID' => $post_id];
if (isset($_POST['post_title'])) {
$title = sanitize_text_field(wp_unslash((string) $_POST['post_title']));
if ($title !== '') {
$postarr['post_title'] = $title;
}
}
if (isset($_POST['comments_open'])) {
$postarr['comment_status'] = !empty($_POST['comments_open']) ? 'open' : 'closed';
}
if (count($postarr) > 1) {
$result = wp_update_post($postarr, true);
if (is_wp_error($result)) {
wp_send_json_error(['msg' => $result->get_error_message()]);
}
}
if (isset($_POST['post_format']) && post_type_supports($post->post_type, 'post-formats')) {
$format = sanitize_key((string) wp_unslash($_POST['post_format']));
set_post_format($post_id, $format === 'standard' ? '' : $format);
}
if ($post->post_type === 'post' && class_exists('\AKSRT\Core\PostMetaBox')) {
$layout = isset($_POST['show_layout']) ? sanitize_key((string) wp_unslash($_POST['show_layout'])) : '';
if ($layout !== '' && !in_array($layout, ['false', 'no_sidebar', 'sidebar_left', 'sidebar_right'], true)) {
$layout = 'false';
}
// 保留白名单之外的既有配置键(封面、视频等),仅覆盖弹窗提交的字段。
// 复选框未勾选时浏览器不会提交键名,必须无条件写入两个开关。
$meta = get_post_meta($post_id, \AKSRT\Core\PostMetaBox::META_KEY, true);
$meta = is_array($meta) ? $meta : [];
if ($layout !== '') {
$meta['show_layout'] = $layout;
}
if (isset($_POST['subtitle'])) {
$meta['subtitle'] = sanitize_text_field(wp_unslash((string) $_POST['subtitle']));
}
$meta['no_article-navs'] = !empty($_POST['no_article-navs']);
$meta['article_maxheight_xz'] = !empty($_POST['article_maxheight_xz']);
update_post_meta($post_id, \AKSRT\Core\PostMetaBox::META_KEY, $meta);
if (isset($_POST['views'])) {
update_post_meta($post_id, \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY, max(0, (int) $_POST['views']));
}
if (isset($_POST['like'])) {
update_post_meta($post_id, '_aksrt_like_count', max(0, (int) $_POST['like']));
}
}
}
private function save_term(int $term_id, string $taxonomy = ''): void {
$term = $taxonomy !== '' ? get_term($term_id, $taxonomy) : get_term($term_id);
if (!$term || is_wp_error($term)) {
wp_send_json_error(['msg' => __('未找到可用数据', 'aksrt')]);
}
if (!current_user_can('manage_categories') && !current_user_can('manage_terms')) {
wp_send_json_error(['msg' => __('权限不足', 'aksrt')]);
}
$data = [];
if (isset($_POST['term_name'])) {
$name = sanitize_text_field(wp_unslash((string) $_POST['term_name']));
if ($name !== '') {
$data['name'] = $name;
}
}
if (isset($_POST['term_description'])) {
$data['description'] = sanitize_textarea_field(wp_unslash((string) $_POST['term_description']));
}
if (!$data) {
return;
}
$result = wp_update_term($term_id, (string) $term->taxonomy, $data);
if (is_wp_error($result)) {
wp_send_json_error(['msg' => $result->get_error_message()]);
}
}
private function post_modal_html(int $post_id, string $post_type): string {
$meta = class_exists('\AKSRT\Core\PostMetaBox')
? \AKSRT\Core\PostMetaBox::get_meta($post_id)
: [];
$views = (int) ($meta['views'] ?? 0);
$like = (int) ($meta['like'] ?? 0);
$is_single = is_single();
$html = '<div class="aksrt-admin-set">';
$html .= '<p class="em09 muted-3-color mb10">' . esc_html(get_the_title($post_id)) . '</p>';
$html .= $this->text_field('post_title', __('标题', 'aksrt'), (string) get_the_title($post_id));
if ($is_single) {
$html .= $this->text_field('subtitle', __('副标题', 'aksrt'), (string) ($meta['subtitle'] ?? ''));
$layout = (string) ($meta['show_layout'] ?? 'false');
$html .= '<div class="relative line-form mb10 modal-line-field"><select name="show_layout" class="line-form-input">'
. '<option value="false"' . selected($layout, 'false', false) . '>' . esc_html__('跟随主题', 'aksrt') . '</option>'
. '<option value="no_sidebar"' . selected($layout, 'no_sidebar', false) . '>' . esc_html__('无侧边栏', 'aksrt') . '</option>'
. '<option value="sidebar_left"' . selected($layout, 'sidebar_left', false) . '>' . esc_html__('侧边栏靠左', 'aksrt') . '</option>'
. '<option value="sidebar_right"' . selected($layout, 'sidebar_right', false) . '>' . esc_html__('侧边栏靠右', 'aksrt') . '</option>'
. '</select><i class="line-form-line"></i></div>';
$html .= $this->number_field('like', __('点赞数', 'aksrt'), $like);
$html .= $this->number_field('views', __('阅读数', 'aksrt'), $views);
$html .= $this->checkbox_field('no_article-navs', __('不显示目录树', 'aksrt'), !empty($meta['no_article-navs']));
$html .= $this->checkbox_field('article_maxheight_xz', __('限制文章最大高度', 'aksrt'), !empty($meta['article_maxheight_xz']));
}
$html .= '<div class="relative line-form mb10 modal-line-field"><select name="comments_open" class="line-form-input">'
. '<option value="open"' . selected(comments_open($post_id), true, false) . '>' . esc_html__('开启评论', 'aksrt') . '</option>'
. '<option value=""' . selected(comments_open($post_id), false, false) . '>' . esc_html__('关闭评论', 'aksrt') . '</option>'
. '</select><i class="line-form-line"></i></div>';
$html .= $this->hidden_fields('post', $post_id, $post_type);
$html .= '<div class="flex jsb mt10"><button type="button" class="but" data-aksrt-modal-close>' . esc_html__('取消', 'aksrt') . '</button>'
. '<button type="submit" class="but jb-blue aksrt-admin-set-submit"><i class="fa fas fa-check"></i> ' . esc_html__('确认修改', 'aksrt') . '</button></div>';
$html .= '</div>';
return $this->wrap_form($html);
}
private function term_modal_html(int $term_id, string $taxonomy): string {
$term = get_term($term_id, $taxonomy);
if (!$term || is_wp_error($term)) {
return '<div class="aksrt-admin-set"><p class="muted-3-color">' . esc_html__('未找到可用数据', 'aksrt') . '</p></div>';
}
$html = '<div class="aksrt-admin-set">';
$html .= '<p class="em09 muted-3-color mb10">' . esc_html($term->name) . '</p>';
$html .= $this->text_field('term_name', __('名称', 'aksrt'), (string) $term->name);
$html .= $this->text_field('term_description', __('描述', 'aksrt'), (string) $term->description);
$html .= $this->hidden_fields('tax', $term_id, $taxonomy);
$html .= '<div class="flex jsb mt10"><button type="button" class="but" data-aksrt-modal-close>' . esc_html__('取消', 'aksrt') . '</button>'
. '<button type="submit" class="but jb-blue aksrt-admin-set-submit"><i class="fa fas fa-check"></i> ' . esc_html__('确认修改', 'aksrt') . '</button></div>';
$html .= '</div>';
return $this->wrap_form($html);
}
private function wrap_form(string $inner): string {
return '<form class="aksrt-admin-set-form">'
. '<input type="hidden" name="action" value="aksrt_frontend_set_save">'
. wp_nonce_field('aksrt_frontend_set', '_wpnonce', true, false)
. $inner
. '</form>';
}
private function hidden_fields(string $type, int $id, string $subtype): string {
return '<input type="hidden" name="type" value="' . esc_attr($type) . '">'
. '<input type="hidden" name="subtype" value="' . esc_attr($subtype) . '">'
. '<input type="hidden" name="id" value="' . esc_attr((string) $id) . '">';
}
private function text_field(string $name, string $label, string $value): string {
return '<div class="relative line-form mb10 modal-line-field"><input type="text" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '" class="line-form-input">'
. '<div class="scale-placeholder">' . esc_html($label) . '</div><i class="line-form-line"></i></div>';
}
private function number_field(string $name, string $label, int $value): string {
return '<div class="relative line-form mb10 modal-line-field"><input type="number" min="0" step="1" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '" class="line-form-input">'
. '<div class="scale-placeholder">' . esc_html($label) . '</div><i class="line-form-line"></i></div>';
}
private function checkbox_field(string $name, string $label, bool $checked): string {
return '<div class="mb10 form-switch-label"><label><input type="checkbox" name="' . esc_attr($name) . '" value="1"' . checked($checked, true, false) . '> ' . esc_html($label) . '</label></div>';
}
}
+49
View File
@@ -189,6 +189,55 @@ class ExtensionRuntime {
];
}
/** 获取系统工具设置(WordPress 优化、上传优化、编辑器与显示)。 */
public static function get_system_tools_settings(): array {
$settings = Settings::get_all();
$switch = static function (string $key, bool $default) use ($settings): bool {
return !isset($settings[$key]) ? $default : !empty($settings[$key]);
};
$rename_type = sanitize_key((string) ($settings['newfilename_type'] ?? 'time'));
if (!in_array($rename_type, ['random', 'time'], true)) {
$rename_type = 'time';
}
// Only fill defaults when the key was never saved — empty array means "none".
$toolbar_scenes = ['pc_s', 'm_s'];
if (!array_key_exists('mce_float_toolbar', $settings)) {
$float_toolbar = $toolbar_scenes;
} else {
$float_toolbar = [];
foreach ((array) ($settings['mce_float_toolbar'] ?? []) as $scene) {
$scene = sanitize_key((string) $scene);
if (in_array($scene, $toolbar_scenes, true) && !in_array($scene, $float_toolbar, true)) {
$float_toolbar[] = $scene;
}
}
}
return [
'hide_admin_bar' => $switch('hide_admin_bar', true),
'remove_emoji' => $switch('remove_emoji', true),
'remove_open_sans' => $switch('remove_open_sans', true),
'remove_more_wp_head' => $switch('remove_more_wp_head', true),
'remove_dashboard_widgets' => $switch('remove_dashboard_widgets', true),
'display_wp_update' => $switch('display_wp_update', true),
'split_upload_s' => $switch('split_upload_s', true),
'split_minimum_size' => max(4, min(100, (int) ($settings['split_minimum_size'] ?? 20))),
'newfilename' => $switch('newfilename', false),
'newfilename_type' => $rename_type,
'disabled_large_thumbnail' => $switch('disabled_large_thumbnail', true),
'admin_user_del_fields' => $switch('admin_user_del_fields', true),
'admin_frontend_set' => $switch('admin_frontend_set', true),
'close_gutenberg' => $switch('close_gutenberg', false),
'disabled_autoembed' => $switch('disabled_autoembed', true),
'mce_float_toolbar' => $float_toolbar,
'time_ago_s' => $switch('time_ago_s', true),
'time_format' => (string) ($settings['time_format'] ?? 'n月j日 H:i'),
];
}
/** 获取短信接口设置。 */
public static function get_sms_settings(): array {
$settings = Settings::get_all();