PluginProbe
ZIP AI – AI Website Builder & AI Agent / 0.0.12
ZIP AI – AI Website Builder & AI Agent v0.0.12
0.0.13 0.0.12 0.0.11 0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / inc / core / route-schema-builder.php

route-schema-builder.php in ZIP AI – AI Website Builder & AI Agent 0.0.12, at inc/core/route-schema-builder.php

229 lines 6.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Route Schema Builder
4 *
5 * Shared normalizer: converts WP REST route handler args into clean JSON Schema
6 * objects. Used by ExecuteRestRequest (execution) as the single source of truth.
7 *
8 * @package zip-ai
9 */
10
11 namespace ZipAI\MCP\Classes\Core;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Class RouteSchemaBuilder
19 */
20 class RouteSchemaBuilder {
21
22 /**
23 * JSON Schema keys to preserve. All WP-specific keys
24 * (sanitize_callback, validate_callback, context, arg_options, etc.) are stripped.
25 */
26 private const SCHEMA_KEYS = array(
27 'type',
28 'description',
29 'default',
30 'enum',
31 'items',
32 'properties',
33 'required',
34 'anyOf',
35 'oneOf',
36 'allOf',
37 'format',
38 'pattern',
39 'minimum',
40 'maximum',
41 'minItems',
42 'maxItems',
43 'uniqueItems',
44 'additionalProperties',
45 );
46
47 /**
48 * Compute applied defaults for a GET request.
49 *
50 * Returns defaults WordPress silently applies when a param is omitted.
51 * Annotates status=publish with a warning when "any" is a valid value,
52 * so the caller knows drafts are invisible.
53 *
54 * @param string $route REST route (e.g. "/wp/v2/pages").
55 * @param array<array-key, mixed> $explicit_params Params the caller explicitly provided.
56 * @return array<array-key, mixed>
57 */
58 public static function get_applied_defaults( string $route, array $explicit_params ): array {
59 $handler = self::find_route_handler( $route, 'GET' );
60 if ( ! $handler ) {
61 return array();
62 }
63
64 $applied = array();
65
66 $args = isset( $handler['args'] ) && is_array( $handler['args'] ) ? $handler['args'] : array();
67
68 foreach ( $args as $name => $config ) {
69 if ( array_key_exists( $name, $explicit_params ) ) {
70 continue;
71 }
72 if ( ! is_array( $config ) || ! isset( $config['default'] ) ) {
73 continue;
74 }
75
76 $value = $config['default'];
77
78 // Schema-driven warning: only emit when the schema actually includes
79 // "any" as a valid value — prevents false warnings on custom routes.
80 if ( 'status' === $name && 'publish' === $value ) {
81 $schema = self::normalize_arg( $config );
82 if ( self::schema_includes_any( $schema ) ) {
83 $value = 'publish (WARNING: drafts/pending/private hidden — pass status=any to see all)';
84 }
85 }
86
87 $applied[ $name ] = $value;
88 }
89
90 return $applied;
91 }
92
93 /**
94 * Recursively normalize a WP REST arg config into a clean JSON Schema object.
95 *
96 * Preserves all JSON Schema-relevant keys. Strips WP-specific callbacks
97 * (sanitize_callback, validate_callback, context, arg_options, etc.).
98 *
99 * @param array<array-key, mixed> $config WP REST arg config.
100 * @param int $depth Recursion depth guard (max 6).
101 * @return array<array-key, mixed>
102 */
103 public static function normalize_arg( array $config, int $depth = 0 ): array {
104 if ( $depth > 6 ) {
105 return $config;
106 }
107
108 $out = array();
109
110 foreach ( self::SCHEMA_KEYS as $key ) {
111 if ( ! array_key_exists( $key, $config ) ) {
112 continue;
113 }
114
115 $val = $config[ $key ];
116
117 if ( 'items' === $key && is_array( $val ) ) {
118 $out[ $key ] = self::normalize_arg( $val, $depth + 1 );
119
120 } elseif ( 'properties' === $key && is_array( $val ) ) {
121 $props = array();
122 foreach ( $val as $prop_name => $prop_schema ) {
123 $props[ $prop_name ] = is_array( $prop_schema )
124 ? self::normalize_arg( $prop_schema, $depth + 1 )
125 : $prop_schema;
126 }
127 $out[ $key ] = $props;
128
129 } elseif ( in_array( $key, array( 'anyOf', 'oneOf', 'allOf' ), true ) && is_array( $val ) ) {
130 $d = $depth;
131 $out[ $key ] = array_map(
132 function ( $s ) use ( $d ) {
133 return is_array( $s ) ? self::normalize_arg( $s, $d + 1 ) : $s;
134 },
135 $val
136 );
137
138 } elseif ( 'additionalProperties' === $key && is_array( $val ) ) {
139 $out[ $key ] = self::normalize_arg( $val, $depth + 1 );
140
141 } else {
142 $out[ $key ] = $val;
143 }
144 }
145
146 return $out;
147 }
148
149 /**
150 * Check whether a normalized schema includes "any" as a valid value.
151 *
152 * Handles top-level enum, items.enum (array types), and anyOf/oneOf branches.
153 * Used to gate the status=publish warning to only routes that actually support
154 * status=any (i.e., standard WP content endpoints).
155 *
156 * @param array<array-key, mixed> $schema Normalized schema from normalize_arg().
157 * @return bool
158 */
159 public static function schema_includes_any( array $schema ): bool {
160 // Top-level enum (e.g. string type with enum).
161 if (
162 isset( $schema['enum'] ) &&
163 is_array( $schema['enum'] ) &&
164 in_array( 'any', $schema['enum'], true )
165 ) {
166 return true;
167 }
168
169 // Recurse into items sub-schema (covers items.enum, items.anyOf, etc.).
170 if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
171 if ( self::schema_includes_any( $schema['items'] ) ) {
172 return true;
173 }
174 }
175
176 // Recurse through anyOf / oneOf / allOf combiners.
177 foreach ( array( 'anyOf', 'oneOf', 'allOf' ) as $combiner ) {
178 if ( ! empty( $schema[ $combiner ] ) && is_array( $schema[ $combiner ] ) ) {
179 foreach ( $schema[ $combiner ] as $sub ) {
180 if ( is_array( $sub ) && self::schema_includes_any( $sub ) ) {
181 return true;
182 }
183 }
184 }
185 }
186
187 return false;
188 }
189
190 /**
191 * Find the matching handler for a route + method.
192 *
193 * @param string $route REST route (leading slash required).
194 * @param string $method HTTP method.
195 * @return array<array-key, mixed>|null
196 */
197 public static function find_route_handler( string $route, string $method ): ?array {
198 if ( strpos( $route, '/' ) !== 0 ) {
199 $route = '/' . $route;
200 }
201
202 $server = rest_get_server();
203 $routes = $server->get_routes();
204
205 foreach ( $routes as $pattern => $handlers ) {
206 if ( ! preg_match( '#^' . $pattern . '$#', $route ) ) {
207 continue;
208 }
209 if ( ! is_array( $handlers ) ) {
210 continue;
211 }
212 foreach ( $handlers as $handler ) {
213 if ( ! is_array( $handler ) || empty( $handler['methods'] ) ) {
214 continue;
215 }
216 $raw_methods = $handler['methods'];
217 $methods = is_array( $raw_methods )
218 ? $raw_methods
219 : array( (string) ( is_scalar( $raw_methods ) ? $raw_methods : '' ) => true );
220 if ( isset( $methods[ $method ] ) ) {
221 return $handler;
222 }
223 }
224 }
225
226 return null;
227 }
228 }
229