Files
wordpress-theme-aksrt/tools/audit-widget-system.php
T
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

333 lines
12 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
declare(strict_types=1);
if (!defined('ABSPATH')) {
define('ABSPATH', __DIR__ . DIRECTORY_SEPARATOR);
}
// Minimal WordPress-compatible stubs keep the schema audit runnable without a site checkout.
if (!function_exists('sanitize_text_field')) {
function sanitize_text_field($value): string {
return trim(preg_replace('/[\r\n\t]+/', ' ', strip_tags((string) $value)) ?? '');
}
}
if (!function_exists('__')) {
function __($text): string {
return (string) $text;
}
}
if (!function_exists('sanitize_textarea_field')) {
function sanitize_textarea_field($value): string {
return trim(strip_tags((string) $value));
}
}
if (!function_exists('wp_kses_post')) {
function wp_kses_post($value): string {
return strip_tags((string) $value, '<a><b><strong><em><br><p><span><ul><ol><li>');
}
}
if (!function_exists('esc_url_raw')) {
function esc_url_raw($value): string {
$value = trim((string) $value);
return filter_var($value, FILTER_VALIDATE_URL) ? $value : '';
}
}
if (!function_exists('absint')) {
function absint($value): int {
return abs((int) $value);
}
}
if (!function_exists('sanitize_hex_color')) {
function sanitize_hex_color($value): string {
$value = (string) $value;
return preg_match('/^#(?:[0-9a-fA-F]{3}){1,2}$/', $value) ? strtolower($value) : '';
}
}
require dirname(__DIR__) . '/vendor/autoload.php';
use Jio\WidgetSystem\FieldSchema;
use Jio\WidgetSystem\PostQuery;
use Jio\WidgetSystem\RendererRegistry;
use Jio\WidgetSystem\WidgetAreas;
use Jio\WidgetSystem\WidgetRegistry;
use Jio\WidgetSystem\WidgetTransfer;
$errors = [];
$expected = [
'search', 'posts_toc', 'quote', 'icon_cards', 'video', 'embed', 'media_cards',
'icon_cover_cards', 'partners', 'combo_info', 'text_title', 'buttons', 'layout_gap',
'tag_cloud', 'recent_comments', 'links', 'notice', 'term_cards', 'term_aggregation',
'hot_posts', 'post_pay', 'posts', 'tab_posts', 'oneline_posts', 'pricing_plans',
'reviews', 'faq', 'stats', 'highlights', 'feature_intro', 'tilt_cases', 'slider',
'slider_media_combo', 'user_card', 'author_card', 'user_ranking', 'user_list',
];
$registry = WidgetRegistry::all();
if (count($registry) !== 37) {
$errors[] = 'Registry must contain exactly 37 widgets; got ' . count($registry) . '.';
}
if (array_keys($registry) !== $expected) {
$errors[] = 'Registry slugs or ordering differ from the frozen contract.';
}
$expected_areas = [
'all_top_fluid', 'all_bottom_fluid', 'all_sidebar_top', 'all_sidebar_bottom',
'home_top_fluid', 'home_top_content', 'home_bottom_content', 'home_bottom_fluid', 'home_sidebar',
'single_top_fluid', 'single_top_content', 'single_bottom_content', 'single_bottom_fluid', 'single_sidebar',
'cat_top_fluid', 'cat_top_content', 'cat_bottom_content', 'cat_bottom_fluid', 'cat_sidebar',
'tag_top_fluid', 'tag_top_content', 'tag_bottom_content', 'tag_bottom_fluid', 'tag_sidebar',
'search_top_fluid', 'search_top_content', 'search_bottom_content', 'search_bottom_fluid', 'search_sidebar',
'author_top_fluid', 'author_bottom_fluid', 'msg_top_fluid', 'msg_bottom_fluid',
'user_top_fluid', 'user_top_content', 'user_bottom_fluid',
'newposts_sidebar_top', 'newposts_sidebar_bottom', 'all_footer', 'mobile_nav_fluid',
];
if (WidgetAreas::static_ids() !== $expected_areas || count(WidgetAreas::static_ids()) !== 40) {
$errors[] = 'Widget area registry must contain the exact 40 static areas.';
}
$templates = WidgetTransfer::templates();
$expected_templates = ['article_home', 'article_list', 'links_navigation', 'article_sidebar', 'knowledge_landing'];
if (array_keys($templates) !== $expected_templates || count($templates) !== 5) {
$errors[] = 'Import/export must expose the exact five built-in templates.';
}
foreach ($templates as $template => $contract) {
foreach ((array) ($contract['sidebars'] ?? []) as $items) {
foreach ((array) $items as $item) {
if (!isset($registry[(string) ($item['widget'] ?? '')])) {
$errors[] = $template . ': template contains an unknown widget.';
}
}
}
}
$supported = FieldSchema::supported_types();
$walk = static function (array $fields, string $owner) use (&$walk, &$errors, $supported): void {
$ids = [];
foreach ($fields as $field) {
$id = (string) ($field['id'] ?? '');
$type = (string) ($field['type'] ?? 'text');
if (!in_array($type, $supported, true)) {
$errors[] = $owner . ': unsupported field type ' . $type . '.';
}
if ($id !== '') {
if (isset($ids[$id])) {
$errors[] = $owner . ': duplicate field id ' . $id . '.';
}
$ids[$id] = true;
}
if (!empty($field['fields']) && is_array($field['fields'])) {
$walk($field['fields'], $owner . '.' . ($id ?: $type));
}
}
};
foreach ($registry as $slug => $definition) {
if ($definition->id_base() !== 'jio_widget_' . $slug) {
$errors[] = $slug . ': invalid id_base.';
}
if (preg_match('/(?:bbs|forum|shop|cart|inventory|shipping)/i', $slug . ' ' . $definition->renderer())) {
$errors[] = $slug . ': forbidden forum/shop contract.';
}
$fields = $definition->fields();
$walk($fields, $slug);
$field_ids = array_column($fields, 'id');
$flags = $definition->common();
$required_common = [];
if (!empty($flags['visibility'])) {
array_push($required_common, 'show_type', 'show_id_type', 'show_ids');
}
if (!empty($flags['layout'])) {
array_push($required_common, 'layout_pin_pc', 'layout_bg');
}
if (!empty($flags['animation'])) {
$required_common[] = 'animation_in';
}
foreach ($required_common as $required) {
if (!in_array($required, $field_ids, true)) {
$errors[] = $slug . ': missing common field ' . $required . '.';
}
}
}
$renderers = RendererRegistry::all();
if (count($renderers) !== 37 || array_keys($renderers) !== $expected) {
$errors[] = 'Renderer registry must map the exact 37 frozen slugs.';
}
foreach ($renderers as $slug => $renderer) {
if (!is_callable([$renderer, 'render'])) {
$errors[] = $slug . ': renderer is not callable.';
}
}
$dynamic = ['posts', 'tab_posts', 'oneline_posts'];
foreach ($expected as $slug) {
if (RendererRegistry::supports_items($slug) !== in_array($slug, $dynamic, true)) {
$errors[] = $slug . ': invalid dynamic items capability.';
}
}
if (!defined('DAY_IN_SECONDS')) {
define('DAY_IN_SECONDS', 86400);
}
if (!function_exists('current_datetime')) {
function current_datetime(): DateTimeImmutable {
return new DateTimeImmutable('now', new DateTimeZone('UTC'));
}
}
$query_args = PostQuery::arguments([
'count' => 7,
'category_ids' => '2,5',
'tag_ids' => '8,13',
'limit_day' => 30,
'orderby' => 'views',
'order' => 'ASC',
'pay_type' => 'video',
], 3);
if (($query_args['posts_per_page'] ?? 0) !== 7 || ($query_args['paged'] ?? 0) !== 3) {
$errors[] = 'Post query pagination contract failed.';
}
if (($query_args['category__in'] ?? []) !== [2, 5] || ($query_args['tag__in'] ?? []) !== [8, 13]) {
$errors[] = 'Post query taxonomy contract failed.';
}
if (($query_args['orderby'] ?? '') !== 'meta_value_num' || ($query_args['order'] ?? '') !== 'ASC') {
$errors[] = 'Post query ordering contract failed.';
}
$pay_query = json_encode($query_args['meta_query'] ?? []);
if (!is_string($pay_query) || !str_contains($pay_query, 'video')) {
$errors[] = 'Post query paid-content contract failed.';
}
$rest_source = (string) file_get_contents(dirname(__DIR__) . '/src/WidgetSystem/RestController.php');
foreach ([
"'permission_callback' => '__return_true'",
"'instance' => ['type' => 'string', 'required' => true",
"'page' => ['type' => 'integer'",
"'segment' => ['type' => 'string'",
'wp_get_sidebars_widgets()',
'$widget->get_settings()',
] as $contract) {
if (!str_contains($rest_source, $contract)) {
$errors[] = 'REST controller missing contract: ' . $contract;
}
}
if (preg_match('/\$_(?:GET|POST|REQUEST)/', $rest_source)) {
$errors[] = 'REST controller must use WP_REST_Request only.';
}
$frontend_files = [
dirname(__DIR__) . '/assets/widgets/frontend.css',
dirname(__DIR__) . '/assets/widgets/frontend.js',
];
foreach ($frontend_files as $frontend_file) {
if (!is_file($frontend_file) || filesize($frontend_file) === 0) {
$errors[] = 'Missing frontend widget asset: ' . basename($frontend_file);
}
}
$root = dirname(__DIR__);
$legacy_paths = [
'/src/Core/WidgetAjax.php',
'/src/Core/SetupPart1.php',
'/src/Widgets',
'/inc/modules/widgets-register.php',
'/inc/modules/widget-common.php',
'/inc/modules/widget-areas.php',
'/assets/css/frontend/widgets.css',
'/assets/css/frontend/widgets',
'/assets/css/frontend/widget-batch5.css',
'/assets/js/widget-set.js',
];
foreach ($legacy_paths as $legacy_path) {
if (file_exists($root . $legacy_path)) {
$errors[] = 'Legacy widget runtime remains: ' . $legacy_path;
}
}
$runtime_source = (string) file_get_contents($root . '/src/WidgetSystem/Runtime.php');
foreach (['WidgetAreas::class', 'WidgetRegistry::all()', 'register_widget(new SchemaWidget', 'unregister_core_widgets'] as $contract) {
if (!str_contains($runtime_source, $contract)) {
$errors[] = 'Widget runtime missing contract: ' . $contract;
}
}
$asset_source = (string) file_get_contents($root . '/src/WidgetSystem/AssetManager.php');
if (str_contains($asset_source, 'cdn.jsdelivr.net')) {
$errors[] = 'Widget assets must not use CDN dependencies.';
}
$admin_import_sources = (string) file_get_contents($root . '/src/Admin/ImportExportController.php')
. (string) file_get_contents($root . '/src/Admin/ImportExportControllerPart1.php');
foreach (['/widgets/export', '/widgets/import', '/widgets/templates', 'jio_widget_templates'] as $legacy_contract) {
if (str_contains($admin_import_sources, $legacy_contract)) {
$errors[] = 'Legacy widget import/export contract remains: ' . $legacy_contract;
}
}
$pinned_assets = [
'DPlayer.min.js' => '361689807fc1e856f868df91036e41195d2c255619bd1c5526849c5ad7491241',
'hls.min.js' => 'e876282a26116e323c8ed3e86bed3838a80dbbe3a2faa09f834bee86963ff760',
'flv.min.js' => '0668d36757cdc17804d5c0bd6fe8956d76370e73a733bf5bbc0eddbfbca2c6cc',
'dash.all.min.js' => '3e753969c16a2a78f420a25d24869f14721eb1d284418f9f22d35a31c3693cd9',
'vanilla-tilt.min.js' => 'de6fa8d3f40dbae2726a72f7d1ae46bb0588267af05355bef1d128f737f4a282',
];
foreach ($pinned_assets as $asset => $hash) {
$path = $root . '/assets/widgets/third-party/' . $asset;
if (!is_file($path)) {
$errors[] = 'Missing pinned widget dependency: ' . $asset;
} else {
$contents = (string) file_get_contents($path);
$raw_hash = hash('sha256', $contents);
$lf_hash = hash('sha256', str_replace("\r\n", "\n", $contents));
if ($raw_hash !== $hash && $lf_hash !== $hash) {
$errors[] = 'Pinned widget dependency changed: ' . $asset;
}
}
}
if (!is_file($root . '/assets/widgets/third-party/THIRD_PARTY.md')) {
$errors[] = 'Missing widget dependency license manifest.';
}
$posts = $registry['posts'];
$normalized = $posts->normalize([
'title' => '<script>alert(1)</script> 最新课程',
'show_type' => 'invalid',
'show_id_type' => 'show',
'show_ids' => '3, 3,9 bad -4',
'count' => 999,
'style' => 'broken',
'cat' => '4,-7',
]);
if ($normalized['count'] !== 20 || $normalized['style'] !== 'list' || $normalized['show_type'] !== 'all') {
$errors[] = 'Schema clamping or enum fallback failed.';
}
if ($normalized['show_ids'] !== '3,9,4' || $normalized['cat'] !== '4,-7') {
$errors[] = 'ID list normalization failed: ' . $normalized['show_ids'];
}
if (str_contains($normalized['title'], '<script>')) {
$errors[] = 'Text sanitization failed.';
}
$baseline_file = dirname(__DIR__) . '/.specs/baselines/widget-reference.json';
$baseline = json_decode((string) file_get_contents($baseline_file), true);
if (!is_array($baseline) || ($baseline['source_policy']['modern_widget_count'] ?? null) !== 37) {
$errors[] = 'Reference baseline is missing or invalid.';
} elseif (is_dir((string) ($baseline['source_root'] ?? ''))) {
foreach ((array) ($baseline['files'] ?? []) as $file) {
$path = rtrim((string) $baseline['source_root'], '\\/') . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, (string) $file['path']);
if (!is_file($path)) {
$errors[] = 'Reference file missing: ' . $path;
continue;
}
if (hash_file('sha256', $path) !== ($file['sha256'] ?? '')) {
$errors[] = 'Reference file changed: ' . $file['path'];
}
}
}
if ($errors) {
fwrite(STDERR, "Widget system audit failed:\n- " . implode("\n- ", $errors) . "\n");
exit(1);
}
echo "Widget system audit passed: 37 widgets, 40 areas, 5 templates, single runtime and pinned assets.\n";