Skip to content

Commit 440d7e1

Browse files
authored
JSON Schemas: Add framework and Post Type schema (#221)
* Create validator class * Include validator class * Add the post type schema * Add test fixtures * Update schema to accept single or array items * Logically group properties, improve descriptions * Fix validation, exclude WordPress reserved terms * Add tests, remove duplicate fixtures included in the new data provider * Format real-exported-file.json * Fix PHPStan issue * Update composer.lock * Remove ID metadata property * Mark some properties as export-only
1 parent f13634e commit 440d7e1

File tree

11 files changed

+1715
-503
lines changed

11 files changed

+1715
-503
lines changed

bin/baseline.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,9 @@ parameters:
109109
identifier: isset.variable
110110
count: 1
111111
path: ../includes/locations/abstract-acf-legacy-location.php
112+
113+
-
114+
message: '#^Path in require_once\(\) "\./wp\-admin/includes/file\.php" is not a file or it does not exist\.$#'
115+
identifier: requireOnce.fileNotFound
116+
count: 2
117+
path: ../includes/class-scf-json-schema-validator.php

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
}
1010
],
1111
"require": {
12-
"php": ">=7.4"
12+
"php": ">=7.4",
13+
"justinrainbow/json-schema": "^5.2"
1314
},
1415
"require-dev": {
1516
"automattic/wordbless": "^0.5.0",

composer.lock

Lines changed: 413 additions & 502 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
<?php
2+
/**
3+
* JSON Schema Validator for SCF entities
4+
*
5+
* @package SCF
6+
*/
7+
8+
if ( ! defined( 'ABSPATH' ) ) {
9+
exit; // Exit if accessed directly
10+
}
11+
12+
if ( ! class_exists( 'SCF_JSON_Schema_Validator' ) ) :
13+
14+
/**
15+
* SCF JSON Schema Validator
16+
*
17+
* Validates JSON data against schemas for SCF entities. Currently supports post types.
18+
* Uses the justinrainbow/json-schema library for validation.
19+
*
20+
* @since SCF 6.x
21+
*/
22+
class SCF_JSON_Schema_Validator {
23+
24+
25+
/**
26+
* The last validation errors.
27+
*
28+
* @var array
29+
*/
30+
private $validation_errors = array();
31+
32+
/**
33+
* Base path for schema files.
34+
*
35+
* @var string
36+
*/
37+
private $schema_path;
38+
39+
/**
40+
* Constructor.
41+
*/
42+
public function __construct() {
43+
$this->schema_path = acf_get_path( 'schemas/' );
44+
}
45+
46+
47+
48+
/**
49+
* Smart validation method that auto-detects input type.
50+
*
51+
* @param mixed $input File path, JSON string, or parsed data to validate.
52+
* @param string $schema_name The name of the schema file (without .schema.json extension).
53+
* @return bool True if valid, false otherwise.
54+
*/
55+
public function validate( $input, $schema_name ) {
56+
// Auto-detect input type and handle appropriately
57+
if ( is_string( $input ) ) {
58+
if ( file_exists( $input ) ) {
59+
// It's a file path
60+
return $this->validate_file( $input, $schema_name );
61+
} else {
62+
// It's a JSON string
63+
return $this->validate_json( $input, $schema_name );
64+
}
65+
}
66+
// It's already parsed data
67+
return $this->validate_data( $input, $schema_name );
68+
}
69+
70+
/**
71+
* Validates parsed data against a schema.
72+
*
73+
* @param array|object $data The data to validate (arrays are converted to objects).
74+
* @param string $schema_name The name of the schema file (without .schema.json extension).
75+
* @return bool True if valid, false otherwise.
76+
*/
77+
public function validate_data( $data, $schema_name ) {
78+
$this->clear_validation_errors();
79+
80+
$schema = $this->load_schema( $schema_name );
81+
if ( ! $schema ) {
82+
$this->add_validation_error( 'system', 'Failed to load schema: ' . $schema_name );
83+
return false;
84+
}
85+
86+
// Convert arrays to objects recursively for JsonSchema validation (library expects objects)
87+
if ( is_array( $data ) ) {
88+
$data = json_decode( wp_json_encode( $data ) );
89+
}
90+
91+
// Create schema storage and register schemas for $ref support
92+
$schema_storage = new JsonSchema\SchemaStorage();
93+
94+
// Register common schema
95+
$common_schema_path = $this->schema_path . 'common.schema.json';
96+
$common_schema_content = wp_json_file_decode( $common_schema_path );
97+
$schema_storage->addSchema( 'file://common.schema.json', $common_schema_content );
98+
99+
// Register main schema
100+
$main_schema_uri = 'file://' . $schema_name . '.schema.json';
101+
$schema_storage->addSchema( $main_schema_uri, $schema );
102+
103+
$validator = new JsonSchema\Validator( new JsonSchema\Constraints\Factory( $schema_storage ) );
104+
$validator->validate( $data, $schema );
105+
106+
foreach ( $validator->getErrors() as $error ) {
107+
$this->add_validation_error( $error['property'], $error['message'] );
108+
}
109+
110+
return $validator->isValid();
111+
}
112+
113+
/**
114+
* Loads a schema file.
115+
*
116+
* @param string $schema_name The name of the schema file (without .schema.json extension).
117+
* @return object|null The loaded schema object, or null on failure.
118+
*/
119+
public function load_schema( $schema_name ) {
120+
$schema_file = $this->schema_path . $schema_name . '.schema.json';
121+
122+
if ( ! file_exists( $schema_file ) ) {
123+
return null;
124+
}
125+
126+
if ( ! function_exists( 'WP_Filesystem' ) ) {
127+
require_once ABSPATH . 'wp-admin/includes/file.php';
128+
}
129+
WP_Filesystem();
130+
global $wp_filesystem;
131+
$schema_content = $wp_filesystem->get_contents( $schema_file );
132+
if ( false === $schema_content ) {
133+
return null;
134+
}
135+
136+
try {
137+
return json_decode( $schema_content, false, 512, JSON_THROW_ON_ERROR );
138+
} catch ( JsonException $e ) {
139+
return null;
140+
}
141+
}
142+
143+
/**
144+
* Gets the validation errors from the last validation attempt.
145+
*
146+
* @return array Array of validation errors with 'field' and 'message' keys.
147+
*/
148+
public function get_validation_errors() {
149+
return $this->validation_errors;
150+
}
151+
152+
/**
153+
* Checks if there are any validation errors.
154+
*
155+
* @return bool True if there are validation errors, false otherwise.
156+
*/
157+
public function has_validation_errors() {
158+
return ! empty( $this->validation_errors );
159+
}
160+
161+
/**
162+
* Gets validation errors formatted as a string.
163+
*
164+
* @param string $separator The separator between error messages.
165+
* @return string The formatted error message.
166+
*/
167+
public function get_validation_errors_string( $separator = '; ' ) {
168+
$messages = array();
169+
foreach ( $this->validation_errors as $error ) {
170+
$field_info = ! empty( $error['field'] ) ? '[' . $error['field'] . '] ' : '';
171+
$messages[] = $field_info . $error['message'];
172+
}
173+
return implode( $separator, $messages );
174+
}
175+
176+
/**
177+
* Adds a validation error.
178+
*
179+
* @param string $field The field that has the error.
180+
* @param string $message The error message.
181+
*/
182+
private function add_validation_error( $field, $message ) {
183+
$this->validation_errors[] = array(
184+
'field' => $field,
185+
'message' => $message,
186+
);
187+
}
188+
189+
/**
190+
* Clears all validation errors.
191+
*/
192+
private function clear_validation_errors() {
193+
$this->validation_errors = array();
194+
}
195+
196+
197+
198+
/**
199+
* Validates JSON string data.
200+
*
201+
* @param string $json_string The JSON string to validate.
202+
* @param string $schema_name The name of the schema to validate against.
203+
* @return bool True if valid, false otherwise.
204+
*/
205+
public function validate_json( $json_string, $schema_name ) {
206+
$this->clear_validation_errors();
207+
208+
try {
209+
$data = json_decode( $json_string, false, 512, JSON_THROW_ON_ERROR );
210+
} catch ( JsonException $e ) {
211+
$this->add_validation_error( 'json', 'Invalid JSON: ' . $e->getMessage() );
212+
return false;
213+
}
214+
215+
return $this->validate_data( $data, $schema_name );
216+
}
217+
218+
/**
219+
* Validates a JSON file.
220+
*
221+
* @param string $file_path Path to the JSON file.
222+
* @param string $schema_name The name of the schema to validate against.
223+
* @return bool True if valid, false otherwise.
224+
*/
225+
public function validate_file( $file_path, $schema_name ) {
226+
$this->clear_validation_errors();
227+
228+
if ( ! file_exists( $file_path ) ) {
229+
$this->add_validation_error( 'file', 'File does not exist: ' . $file_path );
230+
return false;
231+
}
232+
233+
if ( ! function_exists( 'WP_Filesystem' ) ) {
234+
require_once ABSPATH . 'wp-admin/includes/file.php';
235+
}
236+
WP_Filesystem();
237+
global $wp_filesystem;
238+
$json_content = $wp_filesystem->get_contents( $file_path );
239+
240+
if ( false === $json_content ) {
241+
$this->add_validation_error( 'file', 'Could not read file: ' . $file_path );
242+
return false;
243+
}
244+
245+
return $this->validate_json( $json_content, $schema_name );
246+
}
247+
}
248+
249+
endif; // class_exists check

schemas/common.schema.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$id": "https://raw.githubusercontent.com/WordPress/secure-custom-fields/trunk/schemas/common.schema.json",
4+
"title": "SCF Common Definitions",
5+
"description": "Shared definitions for SCF schemas",
6+
"definitions": {
7+
"wordpressReservedTerms": {
8+
"enum": [
9+
"action",
10+
"attachment",
11+
"author",
12+
"category",
13+
"comment",
14+
"custom_css",
15+
"customize_changeset",
16+
"day",
17+
"feed",
18+
"hour",
19+
"link_category",
20+
"minute",
21+
"month",
22+
"name",
23+
"nav_menu_item",
24+
"oembed_cache",
25+
"order",
26+
"orderby",
27+
"page",
28+
"paged",
29+
"post",
30+
"post_format",
31+
"post_tag",
32+
"post_type",
33+
"revision",
34+
"search",
35+
"second",
36+
"tag",
37+
"taxonomy",
38+
"term",
39+
"theme",
40+
"type",
41+
"user_request",
42+
"w",
43+
"wp_block",
44+
"wp_global_styles",
45+
"wp_navigation",
46+
"wp_template",
47+
"wp_template_part",
48+
"year"
49+
]
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)