Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsSchema
Validation{} │ WC 1.0
Service class validating checkout field schema.
No Hooks.
Usage
$Validation = new Validation(); // use class methods
Methods
- public static get_field_schema_with_context( $field_id, $field_schema, $context )
- public static has_field_schema( $fields )
- public static is_valid_schema( $rules )
- public static validate_document_object( DocumentObject $document_object, $rules )
- private static build_meta_schema()
- private static format_schema_error( ValidationError $error )
- private static schema_is_unwrapped( $schema )
Validation{} Validation{} code WC 11.2.1
class Validation {
/**
* Meta schema.
*
* @var string
*/
private static $meta_schema_json = '';
/**
* Date comparison keywords shared with ajv-formats.
*/
private const FORMAT_LIMIT_KEYWORDS = [
'formatMinimum',
'formatMaximum',
'formatExclusiveMinimum',
'formatExclusiveMaximum',
];
/**
* Keywords that may hold a `$data` reference instead of a literal value, because
* the base document schema we have doesn't include those.
*/
private const DATA_REF_KEYWORDS = [
'multipleOf',
'maximum',
'exclusiveMaximum',
'minimum',
'exclusiveMinimum',
'maxLength',
'minLength',
'pattern',
'maxItems',
'minItems',
'uniqueItems',
'maxProperties',
'minProperties',
'required',
'enum',
'format',
];
/**
* Get the field schema with context.
*
* @param string $field_id The field ID.
* @param array $field_schema The field schema.
* @param string $context The context.
* @return array
*/
public static function get_field_schema_with_context( $field_id, $field_schema, $context ) {
$primary_key = 'checkout';
$secondary_key = 'additional_fields';
switch ( $context ) {
case 'billing_address':
case 'shipping_address':
$primary_key = 'customer';
$secondary_key = $context;
break;
case 'contact':
$primary_key = 'customer';
$secondary_key = 'additional_fields';
break;
}
return [
$primary_key => [
'properties' => [
$secondary_key => [
'properties' => [
$field_id => $field_schema,
],
],
],
],
];
}
/**
* Check if the schema is unwrapped (has cart, checkout, customer, as top level keys).
*
* @param array $schema The schema to check.
* @return bool
*/
private static function schema_is_unwrapped( $schema ) {
return isset( $schema['cart'] ) || isset( $schema['checkout'] ) || isset( $schema['customer'] );
}
/**
* Validate the field rules.
*
* @param DocumentObject $document_object The document object to validate.
* @param array $rules The rules to validate against.
* @return bool|WP_Error
*/
public static function validate_document_object( DocumentObject $document_object, $rules ) {
if ( self::schema_is_unwrapped( $rules ) ) {
$rules = [
'$schema' => 'http://json-schema.org/draft-07/schema#',
'type' => 'object',
'properties' => $rules,
];
} else {
if ( ! isset( $rules['$schema'] ) ) {
$rules['$schema'] = 'http://json-schema.org/draft-07/schema#';
}
if ( ! isset( $rules['type'] ) ) {
$rules['type'] = 'object';
}
}
try {
$vocabulary = new DefaultVocabulary();
foreach ( self::FORMAT_LIMIT_KEYWORDS as $keyword ) {
$vocabulary->appendKeyword( new DateFormatLimitParser( $keyword ) );
}
$validator = new Validator( new SchemaLoader( new SchemaParser( [], [], $vocabulary ) ) );
$result = $validator->validate(
Helper::toJSON( $document_object->get_data() ),
Helper::toJSON( $rules )
);
if ( ! $result->hasError() ) {
return true;
}
} catch ( \Exception $e ) {
return new WP_Error( 'woocommerce_rest_checkout_validation_failed', __( 'Validation failed.', 'woocommerce' ) );
}
// Return generic error message.
return new WP_Error( 'woocommerce_rest_checkout_invalid_field', __( 'Invalid field.', 'woocommerce' ) );
}
/**
* Check if the fields have defined schema.
*
* @param array $fields The fields.
* @return bool
*/
public static function has_field_schema( $fields ) {
$return = false;
foreach ( $fields as $field ) {
if (
( ! empty( $field['validation'] ) && is_array( $field['validation'] ) ) ||
( ! empty( $field['required'] ) && is_array( $field['required'] ) ) ||
( ! empty( $field['hidden'] ) && is_array( $field['hidden'] ) )
) {
$return = true;
break;
}
}
return $return;
}
/**
* Validate meta schema for field rules.
*
* @param mixed $rules The rules to validate.
* @return bool|WP_Error True if the field options are valid, a WP_Error otherwise.
*/
public static function is_valid_schema( $rules ) {
if ( ! is_array( $rules ) ) {
return new WP_Error( 'woocommerce_rest_checkout_invalid_field_schema', 'Rules must be defined as an array.' );
}
if ( empty( $rules ) ) {
return true;
}
if ( self::schema_is_unwrapped( $rules ) ) {
$rules = [
'type' => 'object',
'properties' => $rules,
];
}
if ( empty( self::$meta_schema_json ) ) {
self::$meta_schema_json = self::build_meta_schema();
}
$validator = new Validator();
$result = $validator->validate(
Helper::toJSON(
[
'$schema' => 'http://json-schema.org/draft-07/schema#',
'type' => 'object',
'properties' => [
'test' => $rules,
],
'required' => [ 'test' ],
]
),
self::$meta_schema_json
);
$error = $result->error();
if ( null !== $error ) {
return new WP_Error( 'woocommerce_rest_checkout_invalid_field_schema', self::format_schema_error( $error ) );
}
return true;
}
/**
* Describes schema errors at the failing keyword instead of the outer schema wrapper.
*
* @param ValidationError $error The schema validation error.
* @return string The error paths and messages.
*/
private static function format_schema_error( ValidationError $error ): string {
$formatter = new ErrorFormatter();
$errors = $formatter->format(
$error,
true,
static function ( ValidationError $error ) use ( $formatter ): string {
if ( 'const' === $error->keyword() ) {
return sprintf( 'The value must be %s', wp_json_encode( $error->args()['const'] ) );
}
$schema = $error->schema()->info()->data();
if ( 'enum' === $error->keyword() && is_object( $schema ) && isset( $schema->enum ) ) {
return sprintf( 'The value must be one of: %s', implode( ', ', array_map( 'wp_json_encode', $schema->enum ) ) );
}
return $formatter->formatErrorMessage( $error );
}
);
$messages = [];
foreach ( $errors as $path => $details ) {
// The properties/test prefix belongs to our meta-schema check, not the supplied rule.
$path = rawurldecode( preg_replace( '#^/properties/test(?=/|$)#', '', $path ) ?? $path );
$messages[] = sprintf( 'At "%s": %s.', $path ? $path : '/', implode( '; ', $details ) );
}
return implode( ' ', $messages );
}
/**
* Adds $data references and date comparisons to the draft-07 meta schema.
*
* @return string The meta schema as JSON.
*/
private static function build_meta_schema(): string {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$draft_07 = (string) file_get_contents( __DIR__ . '/json-schema-draft-07.json' );
$meta_schema = json_decode( $draft_07, true );
// Fall back to the pristine meta schema rather than none at all, so rules are still checked.
if ( ! is_array( $meta_schema ) ) {
return $draft_07;
}
$meta_schema['definitions']['dataRef'] = [
'type' => 'object',
'required' => [ '$data' ],
'properties' => [
'$data' => [
'type' => 'string',
'anyOf' => [
[ 'format' => 'json-pointer' ],
[ 'format' => 'relative-json-pointer' ],
],
],
],
'additionalProperties' => false,
];
foreach ( self::DATA_REF_KEYWORDS as $keyword ) {
$meta_schema['properties'][ $keyword ] = [
'anyOf' => [
$meta_schema['properties'][ $keyword ],
[ '$ref' => '#/definitions/dataRef' ],
],
];
}
foreach ( self::FORMAT_LIMIT_KEYWORDS as $keyword ) {
$meta_schema['properties'][ $keyword ] = [
'anyOf' => [
[
'type' => 'string',
'format' => 'date',
],
[ '$ref' => '#/definitions/dataRef' ],
],
];
$meta_schema['dependencies'][ $keyword ] = [
'required' => [ 'format' ],
'properties' => [ 'format' => [ 'const' => 'date' ] ],
];
}
$widened = wp_json_encode( $meta_schema );
return is_string( $widened ) ? $widened : $draft_07;
}
}