PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / 6.8.1
Secure Custom Fields v6.8.1
6.9.1 6.9.0 6.8.9 6.8.7 6.8.8 6.8.6 6.8.4 6.8.5 trunk 6.4.0-beta1 6.4.0-beta2 6.4.1 6.4.1-beta3 6.4.1-beta4 6.4.1-beta5 6.4.1-beta6 6.4.1-beta7 6.4.2 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.6.0 6.7.0 6.7.1 6.8.0 6.8.1 6.8.2 6.8.3
secure-custom-fields / includes / class-scf-schema-builder.php
secure-custom-fields / includes Last commit date
Blocks 1 year ago Meta 1 year ago abilities 6 months ago admin 8 months ago ajax 4 months ago api 6 months ago fields 4 months ago forms 6 months ago legacy 1 year ago locations 1 year ago post-types 6 months ago rest-api 6 months ago walkers 1 year ago acf-bidirectional-functions.php 1 year ago acf-field-functions.php 8 months ago acf-field-group-functions.php 8 months ago acf-form-functions.php 1 year ago acf-helper-functions.php 1 year ago acf-hook-functions.php 1 year ago acf-input-functions.php 8 months ago acf-internal-post-type-functions.php 8 months ago acf-meta-functions.php 1 year ago acf-post-functions.php 1 year ago acf-post-type-functions.php 1 year ago acf-taxonomy-functions.php 1 year ago acf-user-functions.php 1 year ago acf-utility-functions.php 1 year ago acf-value-functions.php 1 year ago acf-wp-functions.php 1 year ago assets.php 10 months ago blocks.php 6 months ago class-acf-data.php 10 months ago class-acf-internal-post-type.php 6 months ago class-acf-options-page.php 1 year ago class-acf-site-health.php 4 months ago class-scf-json-schema-validator.php 6 months ago class-scf-schema-builder.php 6 months ago compatibility.php 1 year ago deprecated.php 1 year ago fields.php 10 months ago index.php 1 year ago l10n.php 1 year ago local-fields.php 1 year ago local-json.php 1 year ago local-meta.php 1 year ago locations.php 1 year ago loop.php 10 months ago media.php 1 year ago rest-api.php 10 months ago revisions.php 10 months ago scf-ui-options-page-functions.php 1 year ago third-party.php 8 months ago upgrades.php 1 year ago validation.php 10 months ago wpml.php 1 year ago
class-scf-schema-builder.php
262 lines
1 <?php
2 /**
3 * Schema Builder for SCF
4 *
5 * Handles JSON Schema operations like $ref resolution and schema composition.
6 *
7 * @package SCF
8 */
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12 }
13
14 if ( ! class_exists( 'SCF_Schema_Builder' ) ) :
15
16 /**
17 * SCF Schema Builder
18 *
19 * Builds composed field schemas and resolves $ref for WordPress.
20 *
21 * Why $ref resolution:
22 * - WordPress internal validation doesn't understand JSON Schema $ref
23 * - We inline referenced definitions before passing schemas to WP
24 *
25 * Why oneOf composition:
26 * - Field validation requires type-specific rules (text has maxlength, number has min/max)
27 * - Base properties (key, label, name, type, parent) are shared across all types
28 * - oneOf validates "valid text field OR valid number field OR ..."
29 * - Each variant merges base + type-specific properties with additionalProperties: false
30 * - Fallback variant allows unknown types until all 35 field types have schemas
31 *
32 * Schema structure:
33 * - schemas/field-fragments/field-base.schema.json: Base properties shared by all types
34 * - schemas/field-fragments/{category}/{type}.schema.json: Type-specific properties
35 *
36 * @since 6.8.0
37 */
38 class SCF_Schema_Builder {
39
40 /**
41 * Cached composed field schema.
42 *
43 * @var array|null
44 */
45 private ?array $composed_field_schema = null;
46
47 /**
48 * Cached base field schema.
49 *
50 * @var array|null
51 */
52 private ?array $base_schema = null;
53
54 /**
55 * Recursively resolves $ref references in a JSON schema.
56 *
57 * WordPress internal validation doesn't understand JSON Schema $ref,
58 * so we inline referenced definitions before passing schemas to WP.
59 *
60 * Supports two ref formats:
61 * - Internal refs: #/definitions/foo (resolved from root_schema)
62 * - Relative file refs: file.schema.json#/definitions/foo (loaded from base_path)
63 *
64 * @since 6.8.0
65 *
66 * @param array $schema The schema to resolve.
67 * @param array|null $root_schema The root schema containing definitions. If null, uses $schema.
68 * @param string|null $base_path Base path for loading external schema files. Defaults to schemas/.
69 * @return array The resolved schema.
70 */
71 public function resolve_refs( array $schema, ?array $root_schema = null, ?string $base_path = null ): array {
72 $root_schema = $root_schema ?? $schema;
73 $definitions = $root_schema['definitions'] ?? array();
74 $base_path = $base_path ?? acf_get_path( 'schemas/' );
75
76 if ( isset( $schema['$ref'] ) ) {
77 $ref = $schema['$ref'];
78 $resolved = null;
79
80 // Internal ref: #/definitions/path/to/def
81 if ( preg_match( '~^#/definitions/(.+)$~', $ref, $matches ) ) {
82 $resolved = $definitions;
83 foreach ( explode( '/', $matches[1] ) as $part ) {
84 $resolved = $resolved[ $part ] ?? null;
85 }
86 } elseif ( preg_match( '~^([^#]+)#/definitions/(.+)$~', $ref, $matches ) ) {
87 // Relative file ref: file.schema.json#/definitions/path/to/def
88 $file_path = $base_path . $matches[1];
89 $def_path = $matches[2];
90
91 if ( file_exists( $file_path ) ) {
92 $external_content = file_get_contents( $file_path );
93 $external_schema = json_decode( $external_content, true );
94
95 if ( is_array( $external_schema ) ) {
96 $resolved = $external_schema['definitions'] ?? array();
97 foreach ( explode( '/', $def_path ) as $part ) {
98 $resolved = $resolved[ $part ] ?? null;
99 }
100 }
101 }
102 }
103
104 if ( is_array( $resolved ) ) {
105 unset( $schema['$ref'] );
106 return array_merge( $this->resolve_refs( $resolved, $root_schema, $base_path ), $schema );
107 }
108 }
109
110 foreach ( $schema as $key => $value ) {
111 if ( is_array( $value ) ) {
112 $schema[ $key ] = $this->resolve_refs( $value, $root_schema, $base_path );
113 }
114 }
115
116 return $schema;
117 }
118
119 /**
120 * Merges base and type-specific schema properties.
121 *
122 * Uses array_merge for nested arrays: fragment values overwrite base values,
123 * but base-only keys are preserved (e.g., base's "type": "string" is kept
124 * when fragment only provides "enum").
125 *
126 * @since 6.8.0
127 *
128 * @param array $base_props Base properties from field-base.schema.json.
129 * @param array $type_props Type-specific properties from fragment files.
130 * @return array Merged properties.
131 */
132 private function merge_schema_properties( array $base_props, array $type_props ): array {
133 $merged = $base_props;
134
135 foreach ( $type_props as $prop_name => $prop_value ) {
136 if ( isset( $merged[ $prop_name ] ) && is_array( $merged[ $prop_name ] ) && is_array( $prop_value ) ) {
137 // Merge arrays: fragment values overwrite base, but base-only keys preserved.
138 $merged[ $prop_name ] = array_merge( $merged[ $prop_name ], $prop_value );
139 } else {
140 $merged[ $prop_name ] = $prop_value;
141 }
142 }
143
144 return $merged;
145 }
146
147 /**
148 * Composes a field schema with oneOf containing all type variants.
149 *
150 * Each variant merges base field properties with type-specific properties,
151 * enabling complete validation without schema duplication in source files.
152 *
153 * @since 6.8.0
154 *
155 * @return array The composed schema with oneOf variants.
156 */
157 public function compose_field_schema(): array {
158 if ( null !== $this->composed_field_schema ) {
159 return $this->composed_field_schema;
160 }
161
162 // Load and resolve base field schema.
163 $base_schema = $this->load_base_field_schema();
164 $base_def = $base_schema['definitions']['field'] ?? array();
165 $base_props = $base_def['properties'] ?? array();
166
167 // Build oneOf variants for each field type with a schema.
168 $variants = array();
169 $type_schemas = $this->load_type_schemas();
170
171 foreach ( $type_schemas as $type_schema ) {
172 $type_props = $type_schema['properties'] ?? array();
173
174 $variants[] = array(
175 'type' => 'object',
176 'required' => array( 'key', 'label', 'name', 'type', 'parent' ),
177 'properties' => $this->merge_schema_properties( $base_props, $type_props ),
178 'additionalProperties' => $type_schema['additionalProperties'] ?? false,
179 );
180 }
181
182 $this->composed_field_schema = array(
183 'oneOf' => $variants,
184 );
185
186 return $this->composed_field_schema;
187 }
188
189 /**
190 * Loads the base field schema without resolving refs.
191 *
192 * Refs are kept intact so the generated field.schema.json stays compact.
193 * Consumers (like Field Abilities) resolve refs at runtime when needed.
194 *
195 * @since 6.8.0
196 *
197 * @return array The base field schema with refs intact.
198 */
199 private function load_base_field_schema(): array {
200 if ( null === $this->base_schema ) {
201 $schema_path = ACF_PATH . 'schemas/field-fragments/field-base.schema.json';
202 $this->base_schema = json_decode( file_get_contents( $schema_path ), true );
203 }
204
205 return $this->base_schema;
206 }
207
208 /**
209 * Loads all type-specific field schemas from category directories.
210 *
211 * Scans schemas/field-fragments/{category}/ directories for type schema files.
212 *
213 * @since 6.8.0
214 *
215 * @return array Associative array of type => schema data.
216 */
217 private function load_type_schemas(): array {
218 $schemas = array();
219 $fields_path = ACF_PATH . 'schemas/field-fragments/';
220 $category_dirs = glob( $fields_path . '*', GLOB_ONLYDIR );
221
222 if ( ! is_array( $category_dirs ) ) {
223 return $schemas;
224 }
225
226 foreach ( $category_dirs as $category_path ) {
227 // Scan schema files in this category.
228 $files = glob( $category_path . '/*.schema.json' );
229 if ( ! is_array( $files ) ) {
230 continue;
231 }
232
233 foreach ( $files as $file ) {
234 $content = file_get_contents( $file );
235 if ( false === $content ) {
236 continue;
237 }
238
239 $schema = json_decode( $content, true );
240 if ( ! $schema ) {
241 continue;
242 }
243
244 // Extract field type from schema or filename.
245 $type = $schema['properties']['type']['enum'][0]
246 ?? basename( $file, '.schema.json' );
247
248 $schemas[ $type ] = $schema;
249 }
250 }
251
252 return $schemas;
253 }
254 }
255
256 // Initialize only in WordPress context, not in the CLI.
257 if ( function_exists( 'acf_new_instance' ) ) {
258 acf_new_instance( 'SCF_Schema_Builder' );
259 }
260
261 endif;
262