Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bin/baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,9 @@ parameters:
identifier: isset.variable
count: 1
path: ../includes/locations/abstract-acf-legacy-location.php

-
message: '#^Path in require_once\(\) "\./wp\-admin/includes/file\.php" is not a file or it does not exist\.$#'
identifier: requireOnce.fileNotFound
count: 2
path: ../includes/class-scf-json-schema-validator.php
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
}
],
"require": {
"php": ">=7.4"
"php": ">=7.4",
"justinrainbow/json-schema": "^5.2"
},
"require-dev": {
"automattic/wordbless": "^0.5.0",
Expand Down
915 changes: 413 additions & 502 deletions composer.lock

Large diffs are not rendered by default.

249 changes: 249 additions & 0 deletions includes/class-scf-json-schema-validator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
<?php
/**
* JSON Schema Validator for SCF entities
*
* @package SCF
*/

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}

if ( ! class_exists( 'SCF_JSON_Schema_Validator' ) ) :

/**
* SCF JSON Schema Validator
*
* Validates JSON data against schemas for SCF entities. Currently supports post types.
* Uses the justinrainbow/json-schema library for validation.
*
* @since SCF 6.x
*/
class SCF_JSON_Schema_Validator {


/**
* The last validation errors.
*
* @var array
*/
private $validation_errors = array();

/**
* Base path for schema files.
*
* @var string
*/
private $schema_path;

/**
* Constructor.
*/
public function __construct() {
$this->schema_path = acf_get_path( 'schemas/' );
}



/**
* Smart validation method that auto-detects input type.
*
* @param mixed $input File path, JSON string, or parsed data to validate.
* @param string $schema_name The name of the schema file (without .schema.json extension).
* @return bool True if valid, false otherwise.
*/
public function validate( $input, $schema_name ) {
// Auto-detect input type and handle appropriately
if ( is_string( $input ) ) {
if ( file_exists( $input ) ) {
// It's a file path
return $this->validate_file( $input, $schema_name );
} else {
// It's a JSON string
return $this->validate_json( $input, $schema_name );
}
}
// It's already parsed data
return $this->validate_data( $input, $schema_name );
}

/**
* Validates parsed data against a schema.
*
* @param array|object $data The data to validate (arrays are converted to objects).
* @param string $schema_name The name of the schema file (without .schema.json extension).
* @return bool True if valid, false otherwise.
*/
public function validate_data( $data, $schema_name ) {
$this->clear_validation_errors();

$schema = $this->load_schema( $schema_name );
if ( ! $schema ) {
$this->add_validation_error( 'system', 'Failed to load schema: ' . $schema_name );
return false;
}

// Convert arrays to objects recursively for JsonSchema validation (library expects objects)
if ( is_array( $data ) ) {
$data = json_decode( wp_json_encode( $data ) );
}

// Create schema storage and register schemas for $ref support
$schema_storage = new JsonSchema\SchemaStorage();

// Register common schema
$common_schema_path = $this->schema_path . 'common.schema.json';
$common_schema_content = wp_json_file_decode( $common_schema_path );
$schema_storage->addSchema( 'file://common.schema.json', $common_schema_content );

// Register main schema
$main_schema_uri = 'file://' . $schema_name . '.schema.json';
$schema_storage->addSchema( $main_schema_uri, $schema );

$validator = new JsonSchema\Validator( new JsonSchema\Constraints\Factory( $schema_storage ) );
$validator->validate( $data, $schema );

foreach ( $validator->getErrors() as $error ) {
$this->add_validation_error( $error['property'], $error['message'] );
}

return $validator->isValid();
}

/**
* Loads a schema file.
*
* @param string $schema_name The name of the schema file (without .schema.json extension).
* @return object|null The loaded schema object, or null on failure.
*/
public function load_schema( $schema_name ) {
$schema_file = $this->schema_path . $schema_name . '.schema.json';

if ( ! file_exists( $schema_file ) ) {
return null;
}

if ( ! function_exists( 'WP_Filesystem' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
WP_Filesystem();
global $wp_filesystem;
$schema_content = $wp_filesystem->get_contents( $schema_file );
if ( false === $schema_content ) {
return null;
}

try {
return json_decode( $schema_content, false, 512, JSON_THROW_ON_ERROR );
} catch ( JsonException $e ) {
return null;
}
}

/**
* Gets the validation errors from the last validation attempt.
*
* @return array Array of validation errors with 'field' and 'message' keys.
*/
public function get_validation_errors() {
return $this->validation_errors;
}

/**
* Checks if there are any validation errors.
*
* @return bool True if there are validation errors, false otherwise.
*/
public function has_validation_errors() {
return ! empty( $this->validation_errors );
}

/**
* Gets validation errors formatted as a string.
*
* @param string $separator The separator between error messages.
* @return string The formatted error message.
*/
public function get_validation_errors_string( $separator = '; ' ) {
$messages = array();
foreach ( $this->validation_errors as $error ) {
$field_info = ! empty( $error['field'] ) ? '[' . $error['field'] . '] ' : '';
$messages[] = $field_info . $error['message'];
}
return implode( $separator, $messages );
}

/**
* Adds a validation error.
*
* @param string $field The field that has the error.
* @param string $message The error message.
*/
private function add_validation_error( $field, $message ) {
$this->validation_errors[] = array(
'field' => $field,
'message' => $message,
);
}

/**
* Clears all validation errors.
*/
private function clear_validation_errors() {
$this->validation_errors = array();
}



/**
* Validates JSON string data.
*
* @param string $json_string The JSON string to validate.
* @param string $schema_name The name of the schema to validate against.
* @return bool True if valid, false otherwise.
*/
public function validate_json( $json_string, $schema_name ) {
$this->clear_validation_errors();

try {
$data = json_decode( $json_string, false, 512, JSON_THROW_ON_ERROR );
} catch ( JsonException $e ) {
$this->add_validation_error( 'json', 'Invalid JSON: ' . $e->getMessage() );
return false;
}

return $this->validate_data( $data, $schema_name );
}

/**
* Validates a JSON file.
*
* @param string $file_path Path to the JSON file.
* @param string $schema_name The name of the schema to validate against.
* @return bool True if valid, false otherwise.
*/
public function validate_file( $file_path, $schema_name ) {
$this->clear_validation_errors();

if ( ! file_exists( $file_path ) ) {
$this->add_validation_error( 'file', 'File does not exist: ' . $file_path );
return false;
}

if ( ! function_exists( 'WP_Filesystem' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
WP_Filesystem();
global $wp_filesystem;
$json_content = $wp_filesystem->get_contents( $file_path );

if ( false === $json_content ) {
$this->add_validation_error( 'file', 'Could not read file: ' . $file_path );
return false;
}

return $this->validate_json( $json_content, $schema_name );
}
}

endif; // class_exists check
52 changes: 52 additions & 0 deletions schemas/common.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/WordPress/secure-custom-fields/trunk/schemas/common.schema.json",
"title": "SCF Common Definitions",
"description": "Shared definitions for SCF schemas",
"definitions": {
"wordpressReservedTerms": {
"enum": [
"action",
"attachment",
"author",
"category",
"comment",
"custom_css",
"customize_changeset",
"day",
"feed",
"hour",
"link_category",
"minute",
"month",
"name",
"nav_menu_item",
"oembed_cache",
"order",
"orderby",
"page",
"paged",
"post",
"post_format",
"post_tag",
"post_type",
"revision",
"search",
"second",
"tag",
"taxonomy",
"term",
"theme",
"type",
"user_request",
"w",
"wp_block",
"wp_global_styles",
"wp_navigation",
"wp_template",
"wp_template_part",
"year"
]
}
}
}
Loading
Loading