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
@@ -0,0 +1,268 @@
<?php
/** System tools (系统工具) runtime contract harness: settings defaults, hook wiring, schema, time-format gate. */
declare(strict_types=1);
namespace {
define('ABSPATH', __DIR__ . '/');
$GLOBALS['aksrt_system_settings'] = [];
$GLOBALS['aksrt_system_added'] = [];
$GLOBALS['aksrt_system_removed'] = [];
function __(string $text, string $domain = ''): string { return $text; }
function sanitize_key(string $key): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)) ?? ''; }
function is_super_admin(): bool { return (bool) $GLOBALS['aksrt_system_is_super_admin']; }
function current_time(string $type): string { return date('Y-m-d H:i:s'); }
function add_filter(string $hook, $callback, int $priority = 10, int $args = 1): void {
$GLOBALS['aksrt_system_added'][] = ['filter', $hook, $callback, $priority];
}
function add_action(string $hook, $callback, int $priority = 10, int $args = 1): void {
$GLOBALS['aksrt_system_added'][] = ['action', $hook, $callback, $priority];
}
function remove_filter(string $hook, $callback, int $priority = 10): void {
$GLOBALS['aksrt_system_removed'][] = ['filter', $hook, $callback, $priority];
}
function remove_action(string $hook, $callback, int $priority = 10): void {
$GLOBALS['aksrt_system_removed'][] = ['action', $hook, $callback, $priority];
}
function wp_deregister_style(string $handle): void {}
function wp_register_style(string $handle, $src): void {}
function wp_enqueue_style(string $handle, $src = ''): void {}
function remove_image_size(string $name): void {
$GLOBALS['aksrt_system_added'][] = ['remove_image_size', $name, null, 10];
}
function system_tools_check(bool $condition, string $message): void {
if (!$condition) throw new \RuntimeException($message);
}
function system_tools_added(string $type, string $hook): bool {
foreach ($GLOBALS['aksrt_system_added'] as $row) {
if ($row[0] === $type && $row[1] === $hook) return true;
}
return false;
}
function system_tools_removed(string $type, string $hook, string $needle = ''): bool {
foreach ($GLOBALS['aksrt_system_removed'] as $row) {
if ($row[0] !== $type || $row[1] !== $hook) continue;
if ($needle === '') return true;
if (is_array($row[2])) {
$parts = [];
foreach ($row[2] as $part) {
$parts[] = is_object($part) ? get_class($part) : (string) $part;
}
$callback = implode('|', $parts);
} elseif (is_object($row[2])) {
$callback = get_class($row[2]);
} else {
$callback = (string) $row[2];
}
if (strpos($callback, $needle) !== false) return true;
}
return false;
}
function system_tools_reset(): void {
$GLOBALS['aksrt_system_added'] = [];
$GLOBALS['aksrt_system_removed'] = [];
}
require dirname(__DIR__, 2) . '/src/Settings/ExtensionRuntime.php';
require dirname(__DIR__, 2) . '/src/Extensions/SystemTools.php';
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsIP.php';
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsAudit.php';
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsPush.php';
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsSecurity.php';
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsSystem.php';
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensions.php';
require dirname(__DIR__, 2) . '/inc/helpers/template-tags/search-meta.php';
}
namespace AKSRT\Settings {
final class Settings {
public static function get_all(): array { return $GLOBALS['aksrt_system_settings']; }
}
}
namespace AKSRT\SystemToolsHarness {
final class WpEmbedStub {
public function autoembed($content) { return $content; }
public function maybe_run_ajax_cache(): void {}
}
}
namespace {
use AKSRT\Admin\Schema\SchemaExtensions;
use AKSRT\Admin\Schema\SchemaExtensionsSystem;
use AKSRT\Extensions\SystemTools;
use AKSRT\Settings\ExtensionRuntime;
$GLOBALS['aksrt_system_is_super_admin'] = false;
$GLOBALS['wp_embed'] = new \AKSRT\SystemToolsHarness\WpEmbedStub();
// 1) Defaults match the reference theme's 系统工具 defaults.
$defaults = ExtensionRuntime::get_system_tools_settings();
$expected = [
'hide_admin_bar' => true,
'remove_emoji' => true,
'remove_open_sans' => true,
'remove_more_wp_head' => true,
'remove_dashboard_widgets' => true,
'display_wp_update' => true,
'split_upload_s' => true,
'split_minimum_size' => 20,
'newfilename' => false,
'newfilename_type' => 'time',
'disabled_large_thumbnail' => true,
'admin_user_del_fields' => true,
'admin_frontend_set' => true,
'close_gutenberg' => false,
'disabled_autoembed' => true,
'mce_float_toolbar' => ['pc_s', 'm_s'],
'time_ago_s' => true,
'time_format' => 'n月j日 H:i',
];
foreach ($expected as $key => $value) {
system_tools_check(array_key_exists($key, $defaults), "system tools setting missing: {$key}");
system_tools_check($defaults[$key] === $value, "system tools default wrong for {$key}");
}
// 2) Stored values sanitize: rename type whitelist, switch coercion, passthrough format.
$GLOBALS['aksrt_system_settings'] = [
'newfilename_type' => 'bogus',
'newfilename' => 1,
'time_ago_s' => 0,
'time_format' => 'Y/m/d H:i',
'close_gutenberg' => 'yes',
'split_minimum_size' => 500,
'mce_float_toolbar' => ['m_s', 'bogus', 'm_s'],
];
$clean = ExtensionRuntime::get_system_tools_settings();
system_tools_check($clean['newfilename_type'] === 'time', 'bogus rename type should fall back to time');
system_tools_check($clean['newfilename'] === true, 'truthy rename switch should coerce to true');
system_tools_check($clean['time_ago_s'] === false, 'falsy time_ago_s should coerce to false');
system_tools_check($clean['time_format'] === 'Y/m/d H:i', 'time format should pass through');
system_tools_check($clean['close_gutenberg'] === true, 'truthy gutenberg switch should coerce to true');
system_tools_check($clean['split_minimum_size'] === 100, 'split minimum size should clamp to the 4..100 range');
system_tools_check($clean['mce_float_toolbar'] === ['m_s'], 'float toolbar scenes should whitelist and dedupe');
// 2b) Unsaved float toolbar key keeps the both-scenes default; explicit empty stays empty.
$GLOBALS['aksrt_system_settings'] = ['hide_admin_bar' => 0];
$clean = ExtensionRuntime::get_system_tools_settings();
system_tools_check($clean['mce_float_toolbar'] === ['pc_s', 'm_s'], 'float toolbar should default to pc+mobile');
system_tools_check($clean['hide_admin_bar'] === false, 'stored falsy value should not fall back to default');
$GLOBALS['aksrt_system_settings'] = ['mce_float_toolbar' => []];
$clean = ExtensionRuntime::get_system_tools_settings();
system_tools_check($clean['mce_float_toolbar'] === [], 'explicitly empty float toolbar should stay empty');
// 3) Hook wiring with every switch enabled.
$GLOBALS['aksrt_system_settings'] = array_fill_keys(array_keys($expected), true);
$GLOBALS['aksrt_system_settings']['time_format'] = 'n月j日 H:i';
$GLOBALS['aksrt_system_is_super_admin'] = false;
system_tools_reset();
SystemTools::get_instance()->init();
system_tools_check(system_tools_added('filter', 'show_admin_bar'), 'hide_admin_bar should gate show_admin_bar');
system_tools_check(system_tools_added('action', 'init'), 'remove_open_sans should hook init');
system_tools_check(system_tools_added('action', 'after_setup_theme'), 'head cleanup and thumbnail pruning should hook after_setup_theme');
system_tools_check(system_tools_added('action', 'do_meta_boxes'), 'dashboard widget removal should hook do_meta_boxes');
system_tools_check(system_tools_added('filter', 'site_status_tests'), 'site status tests should be filtered');
system_tools_check(system_tools_added('filter', 'pre_http_request'), 'site health http requests should be blocked');
system_tools_check(system_tools_added('action', 'admin_menu'), 'update notices should hook admin_menu');
system_tools_check(system_tools_added('filter', 'automatic_updater_disabled'), 'automatic updates should be disabled');
system_tools_check(system_tools_added('filter', 'use_block_editor_for_post'), 'close_gutenberg should gate the block editor');
system_tools_check(system_tools_added('action', 'show_user_profile'), 'user profile cleanup should hook show_user_profile');
system_tools_check(system_tools_added('action', 'edit_user_profile'), 'user profile cleanup should hook edit_user_profile');
system_tools_check(system_tools_removed('action', 'admin_print_scripts', 'print_emoji_detection_script'), 'emoji scripts should be removed');
system_tools_check(system_tools_removed('filter', 'the_content', 'autoembed'), 'auto embed should be removed from the_content');
system_tools_check(system_tools_removed('action', 'admin_init', '_maybe_update_core'), 'core update checks should be removed');
system_tools_check(system_tools_removed('action', 'init', 'wp_schedule_update_checks'), 'update cron schedule should be removed');
// 4) hide_admin_bar off keeps the admin bar for super admins.
$GLOBALS['aksrt_system_settings']['hide_admin_bar'] = false;
$GLOBALS['aksrt_system_is_super_admin'] = true;
system_tools_reset();
SystemTools::get_instance()->init();
system_tools_check(!system_tools_added('filter', 'show_admin_bar'), 'super admin should keep the admin bar when hide_admin_bar is off');
// 5) Dashboard widget removal edits the dashboard meta boxes only in the normal context.
$tools = SystemTools::get_instance();
$GLOBALS['wp_meta_boxes'] = [
'dashboard' => [
'normal' => [
'high' => ['dashboard_php_nag' => ['id' => 'dashboard_php_nag']],
'core' => ['dashboard_site_health' => ['id' => 'dashboard_site_health']],
],
'side' => [
'core' => ['dashboard_primary' => ['id' => 'dashboard_primary']],
],
],
];
$tools->remove_dashboard_widgets('dashboard', 'normal');
system_tools_check(!isset($GLOBALS['wp_meta_boxes']['dashboard']['normal']['high']['dashboard_php_nag']), 'php nag widget should be removed');
system_tools_check(!isset($GLOBALS['wp_meta_boxes']['dashboard']['normal']['core']['dashboard_site_health']), 'site health widget should be removed');
system_tools_check(!isset($GLOBALS['wp_meta_boxes']['dashboard']['side']['core']['dashboard_primary']), 'primary widget should be removed');
$GLOBALS['wp_meta_boxes']['dashboard']['side']['core']['dashboard_primary'] = ['id' => 'dashboard_primary'];
$tools->remove_dashboard_widgets('dashboard', 'side');
system_tools_check(isset($GLOBALS['wp_meta_boxes']['dashboard']['side']['core']['dashboard_primary']), 'non-normal context should be ignored');
// 6) Site status tests drop page cache and php version probes.
$tests = $tools->filter_site_status_tests([
'async' => ['page_cache' => [], 'dilation' => []],
'direct' => ['php_version' => [], 'updates' => []],
]);
system_tools_check(!isset($tests['async']['page_cache']) && isset($tests['async']['dilation']), 'page_cache test should be dropped');
system_tools_check(!isset($tests['direct']['php_version']) && isset($tests['direct']['updates']), 'php_version test should be dropped');
// 7) Health endpoint blocking is scoped to version/browser/credits checks.
$blocked = $tools->block_site_health_requests(null, [], 'https://api.wordpress.org/core/serve-happy/1.0/');
system_tools_check(is_array($blocked) && $blocked['body'] === '', 'serve-happy requests should be blocked');
$blocked = $tools->block_site_health_requests(null, [], 'https://api.wordpress.org/core/browse-happy/1.0/');
system_tools_check(is_array($blocked) && $blocked['body'] === '', 'browse-happy requests should be blocked');
$passed = $tools->block_site_health_requests('passthrough', [], 'https://api.wordpress.org/plugins/update-check/1.0/');
system_tools_check($passed === 'passthrough', 'update checks should pass through the blocker');
// 8) Large thumbnail pruning removes the reference theme's size list.
$GLOBALS['aksrt_system_added'] = [];
$tools->disable_large_thumbnail();
$sizes_filter = null;
foreach ($GLOBALS['aksrt_system_added'] as $row) {
if ($row[0] === 'filter' && $row[1] === 'intermediate_image_sizes') {
$sizes_filter = $row[2];
}
}
system_tools_check(is_callable($sizes_filter), 'intermediate_image_sizes filter should be registered');
system_tools_check($sizes_filter(['medium_large', '1536x1536', '2048x2048', 'thumbnail']) === ['thumbnail'], 'medium_large/1536x1536/2048x2048 should be pruned');
// 9) Schema registry exposes the 系统工具 page with all fields.
$sections = SchemaExtensions::build()['sections'];
system_tools_check(isset($sections['system']) && $sections['system']['title'] === '系统工具', 'system section should register under extensions');
$keys = [];
foreach ($sections['system']['groups'] as $group) {
foreach ($group['fields'] as $field) {
$keys[] = $field['key'];
}
}
system_tools_check(count($keys) === count(array_unique($keys)), 'system schema field keys must be unique');
foreach (array_keys($expected) as $key) {
system_tools_check(in_array($key, $keys, true), "system schema missing field {$key}");
}
$client_defaults = array_keys($expected);
system_tools_check(count(SchemaExtensionsSystem::build()['groups']) === 3, 'system schema should keep three groups');
// 10) aksrt_time_ago honors the 倒计时显示 switch and custom format.
$past = date('Y-m-d H:i:s', strtotime('-3 days'));
$GLOBALS['aksrt_system_settings'] = [];
system_tools_check(strpos(aksrt_time_ago($past), '天前') !== false, 'relative time should be used by default');
$GLOBALS['aksrt_system_settings'] = ['time_ago_s' => 0, 'time_format' => 'Y年n月j日'];
$stamp = strtotime('-3 days');
system_tools_check(aksrt_time_ago(date('Y-m-d H:i:s', $stamp)) === date('Y年n月j日', $stamp), 'custom time format should apply when countdown display is off');
$GLOBALS['aksrt_system_settings'] = ['time_ago_s' => 0, 'time_format' => ''];
system_tools_check(strpos(aksrt_time_ago($past), '天前') !== false, 'empty custom format should fall back to relative time');
$GLOBALS['aksrt_system_settings'] = ['time_ago_s' => 1];
system_tools_check(strpos(aksrt_time_ago($past), '天前') !== false, 'countdown display should stay relative when enabled');
echo "System tools harness passed\n";
}