1. '); } } 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 AKSRT\WidgetSystem\FieldSchema; use AKSRT\WidgetSystem\PostQuery; use AKSRT\WidgetSystem\RendererRegistry; use AKSRT\WidgetSystem\WidgetAreas; use AKSRT\WidgetSystem\WidgetRegistry; use AKSRT\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() !== 'aksrt_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'); foreach (['show_type', 'show_id_type', 'show_ids', 'animation_in', 'layout_pin_pc', 'layout_bg'] 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', 'hot_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); } $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', 'aksrt_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; } elseif (hash_file('sha256', $path) !== $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' => ' 最新课程', 'show_type' => 'invalid', 'show_id_type' => 'show', 'show_ids' => '3, 3,9 bad -4', 'count' => 999, 'style' => 'broken', 'pay_type' => 'video', ]); if ($normalized['count'] !== 50 || $normalized['style'] !== 'card' || $normalized['show_type'] !== 'all') { $errors[] = 'Schema clamping or enum fallback failed.'; } if ($normalized['show_ids'] !== '3,9,4') { $errors[] = 'ID list normalization failed: ' . $normalized['show_ids']; } if (str_contains($normalized['title'], '