219 lines
7.5 KiB
PHP
219 lines
7.5 KiB
PHP
<?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('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 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' => '<script>alert(1)</script> 最新课程',
|
||
'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'], '<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 definitions/renderers, frozen baseline, query/REST/assets contracts.\n";
|