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\WidgetRegistry; $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.'; } $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); } } $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'], '