Migrate email editor Validator to WP Coding Standard

[MAILPOET-6240]
This commit is contained in:
Jan Lysý
2024-11-01 21:11:11 +01:00
committed by Jan Lysý
parent fc7e06730b
commit ef8f122bb7
16 changed files with 641 additions and 219 deletions

View File

@ -63,7 +63,7 @@ class Template_Preview {
array( array(
'get_callback' => array( $this, 'get_email_theme_preview_css' ), 'get_callback' => array( $this, 'get_email_theme_preview_css' ),
'update_callback' => null, 'update_callback' => null,
'schema' => Builder::string()->toArray(), 'schema' => Builder::string()->to_array(),
) )
); );
} }

View File

@ -41,6 +41,6 @@ class Email_Api_Controller {
* @return array * @return array
*/ */
public function get_email_data_schema(): array { public function get_email_data_schema(): array {
return Builder::object()->toArray(); return Builder::object()->to_array();
} }
} }

View File

@ -109,6 +109,6 @@ class Email_Styles_Schema {
) )
)->nullable(), )->nullable(),
) )
)->toArray(); )->to_array();
} }
} }

View File

@ -1,40 +1,64 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator\Schema; namespace MailPoet\EmailEditor\Validator\Schema;
use MailPoet\EmailEditor\Validator\Schema; use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#oneof-and-anyof /**
* Represents a schema that allows a value to match any of the given schemas.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#oneof-and-anyof
*/
class Any_Of_Schema extends Schema { class Any_Of_Schema extends Schema {
/**
* Schema definition.
*
* @var array[]
*/
protected $schema = array( protected $schema = array(
'anyOf' => array(), 'anyOf' => array(),
); );
/** @param Schema[] $schemas */ /**
* Any_Of_Schema constructor.
*
* @param Schema[] $schemas List of schemas.
*/
public function __construct( public function __construct(
array $schemas array $schemas
) { ) {
foreach ( $schemas as $schema ) { foreach ( $schemas as $schema ) {
$this->schema['anyOf'][] = $schema->toArray(); $this->schema['anyOf'][] = $schema->to_array();
} }
} }
/**
* Returns the schema as an array.
*/
public function nullable(): self { public function nullable(): self {
$null = array( 'type' => 'null' ); $null = array( 'type' => 'null' );
$anyOf = $this->schema['anyOf']; $any_of = $this->schema['anyOf'];
$value = in_array( $null, $anyOf, true ) ? $anyOf : array_merge( $anyOf, array( $null ) ); $value = in_array( $null, $any_of, true ) ? $any_of : array_merge( $any_of, array( $null ) );
return $this->updateSchemaProperty( 'anyOf', $value ); return $this->update_schema_property( 'anyOf', $value );
} }
public function nonNullable(): self { /**
$null = array( 'type' => 'null' ); * Returns the schema as an array.
$anyOf = $this->schema['anyOf']; */
$value = array_filter( public function non_nullable(): self {
$anyOf, $null = array( 'type' => 'null' );
$any_of = $this->schema['any_of'];
$value = array_filter(
$any_of,
function ( $item ) use ( $null ) { function ( $item ) use ( $null ) {
return $item !== $null; return $item !== $null;
} }
); );
return $this->updateSchemaProperty( 'anyOf', $value ); return $this->update_schema_property( 'any_of', $value );
} }
} }

View File

@ -1,28 +1,60 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator\Schema; namespace MailPoet\EmailEditor\Validator\Schema;
use MailPoet\EmailEditor\Validator\Schema; use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#arrays /**
* Represents a schema for an array.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#arrays
*/
class Array_Schema extends Schema { class Array_Schema extends Schema {
/**
* Schema definition.
*
* @var array
*/
protected $schema = array( protected $schema = array(
'type' => 'array', 'type' => 'array',
); );
/**
* Sets the schema for the items in the array.
*
* @param Schema $schema Schema for the items in the array.
*/
public function items( Schema $schema ): self { public function items( Schema $schema ): self {
return $this->updateSchemaProperty( 'items', $schema->toArray() ); return $this->update_schema_property( 'items', $schema->to_array() );
} }
/**
* Sets the minimum number of items in the array.
*
* @param int $value Minimum number of items in the array.
*/
public function minItems( int $value ): self { public function minItems( int $value ): self {
return $this->updateSchemaProperty( 'minItems', $value ); return $this->update_schema_property( 'minItems', $value );
} }
/**
* Sets the maximum number of items in the array.
*
* @param int $value Maximum number of items in the array.
*/
public function maxItems( int $value ): self { public function maxItems( int $value ): self {
return $this->updateSchemaProperty( 'maxItems', $value ); return $this->update_schema_property( 'maxItems', $value );
} }
/**
* Sets the uniqueItems property to true.
*/
public function uniqueItems(): self { public function uniqueItems(): self {
return $this->updateSchemaProperty( 'uniqueItems', true ); return $this->update_schema_property( 'uniqueItems', true );
} }
} }

View File

@ -1,11 +1,25 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator\Schema; namespace MailPoet\EmailEditor\Validator\Schema;
use MailPoet\EmailEditor\Validator\Schema; use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#primitive-types /**
* Represents a schema for a boolean.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#primitive-types
*/
class Boolean_Schema extends Schema { class Boolean_Schema extends Schema {
/**
* Schema definition.
*
* @var array
*/
protected $schema = array( protected $schema = array(
'type' => 'boolean', 'type' => 'boolean',
); );

View File

@ -1,36 +1,75 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator\Schema; namespace MailPoet\EmailEditor\Validator\Schema;
use MailPoet\EmailEditor\Validator\Schema; use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#numbers /**
* Represents a schema for an integer.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#numbers
*/
class Integer_Schema extends Schema { class Integer_Schema extends Schema {
/**
* Schema definition.
*
* @var array
*/
protected $schema = array( protected $schema = array(
'type' => 'integer', 'type' => 'integer',
); );
/**
* Sets the minimum value of the integer.
*
* @param int $value Minimum value of the integer.
*/
public function minimum( int $value ): self { public function minimum( int $value ): self {
return $this->updateSchemaProperty( 'minimum', $value ) return $this->update_schema_property( 'minimum', $value )
->unsetSchemaProperty( 'exclusiveMinimum' ); ->unset_schema_property( 'exclusiveMinimum' );
} }
/**
* Sets the exclusiveMinimum property to true.
*
* @param int $value Minimum value of the integer.
*/
public function exclusiveMinimum( int $value ): self { public function exclusiveMinimum( int $value ): self {
return $this->updateSchemaProperty( 'minimum', $value ) return $this->update_schema_property( 'minimum', $value )
->updateSchemaProperty( 'exclusiveMinimum', true ); ->update_schema_property( 'exclusiveMinimum', true );
} }
/**
* Sets the maximum value of the integer.
*
* @param int $value Maximum value of the integer.
*/
public function maximum( int $value ): self { public function maximum( int $value ): self {
return $this->updateSchemaProperty( 'maximum', $value ) return $this->update_schema_property( 'maximum', $value )
->unsetSchemaProperty( 'exclusiveMaximum' ); ->unset_schema_property( 'exclusiveMaximum' );
} }
/**
* Sets the exclusiveMaximum property to true.
*
* @param int $value Maximum value of the integer.
*/
public function exclusiveMaximum( int $value ): self { public function exclusiveMaximum( int $value ): self {
return $this->updateSchemaProperty( 'maximum', $value ) return $this->update_schema_property( 'maximum', $value )
->updateSchemaProperty( 'exclusiveMaximum', true ); ->update_schema_property( 'exclusiveMaximum', true );
} }
/**
* Sets the multipleOf property.
*
* @param int $value Multiple of the integer.
*/
public function multipleOf( int $value ): self { public function multipleOf( int $value ): self {
return $this->updateSchemaProperty( 'multipleOf', $value ); return $this->update_schema_property( 'multipleOf', $value );
} }
} }

View File

@ -1,11 +1,25 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator\Schema; namespace MailPoet\EmailEditor\Validator\Schema;
use MailPoet\EmailEditor\Validator\Schema; use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#primitive-types /**
* Represents a schema for a null.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#primitive-types
*/
class Null_Schema extends Schema { class Null_Schema extends Schema {
/**
* Schema definition.
*
* @var array
*/
protected $schema = array( protected $schema = array(
'type' => 'null', 'type' => 'null',
); );

View File

@ -1,36 +1,75 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator\Schema; namespace MailPoet\EmailEditor\Validator\Schema;
use MailPoet\EmailEditor\Validator\Schema; use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#numbers /**
* Represents a schema for a number.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#numbers
*/
class Number_Schema extends Schema { class Number_Schema extends Schema {
/**
* Schema definition.
*
* @var array
*/
protected $schema = array( protected $schema = array(
'type' => 'number', 'type' => 'number',
); );
/**
* Sets the minimum value of the number.
*
* @param float $value Minimum value of the number.
*/
public function minimum( float $value ): self { public function minimum( float $value ): self {
return $this->updateSchemaProperty( 'minimum', $value ) return $this->update_schema_property( 'minimum', $value )
->unsetSchemaProperty( 'exclusiveMinimum' ); ->unset_schema_property( 'exclusiveMinimum' );
} }
/**
* Sets the exclusiveMinimum property to true.
*
* @param float $value Minimum value of the number.
*/
public function exclusiveMinimum( float $value ): self { public function exclusiveMinimum( float $value ): self {
return $this->updateSchemaProperty( 'minimum', $value ) return $this->update_schema_property( 'minimum', $value )
->updateSchemaProperty( 'exclusiveMinimum', true ); ->update_schema_property( 'exclusiveMinimum', true );
} }
/**
* Sets the maximum value of the number.
*
* @param float $value Maximum value of the number.
*/
public function maximum( float $value ): self { public function maximum( float $value ): self {
return $this->updateSchemaProperty( 'maximum', $value ) return $this->update_schema_property( 'maximum', $value )
->unsetSchemaProperty( 'exclusiveMaximum' ); ->unset_schema_property( 'exclusiveMaximum' );
} }
/**
* Sets the exclusiveMaximum property to true.
*
* @param float $value Maximum value of the number.
*/
public function exclusiveMaximum( float $value ): self { public function exclusiveMaximum( float $value ): self {
return $this->updateSchemaProperty( 'maximum', $value ) return $this->update_schema_property( 'maximum', $value )
->updateSchemaProperty( 'exclusiveMaximum', true ); ->update_schema_property( 'exclusiveMaximum', true );
} }
/**
* Sets the multipleOf property.
*
* @param float $value Multiple of the number.
*/
public function multipleOf( float $value ): self { public function multipleOf( float $value ): self {
return $this->updateSchemaProperty( 'multipleOf', $value ); return $this->update_schema_property( 'multipleOf', $value );
} }
} }

View File

@ -1,56 +1,92 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator\Schema; namespace MailPoet\EmailEditor\Validator\Schema;
use MailPoet\EmailEditor\Validator\Schema; use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#objects /**
* Represents a schema for an object.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#objects
*/
class Object_Schema extends Schema { class Object_Schema extends Schema {
/**
* Schema definition.
*
* @var array
*/
protected $schema = array( protected $schema = array(
'type' => 'object', 'type' => 'object',
); );
/** @param array<string, Schema> $properties */ /**
* Set the required properties of the object.
*
* @param array<string, Schema> $properties Required properties.
*/
public function properties( array $properties ): self { public function properties( array $properties ): self {
return $this->updateSchemaProperty( return $this->update_schema_property(
'properties', 'properties',
array_map( array_map(
function ( Schema $property ) { function ( Schema $property ) {
return $property->toArray(); return $property->to_array();
}, },
$properties $properties
) )
); );
} }
/**
* Set the required properties of the object.
*
* @param Schema $schema Schema of the additional properties.
*/
public function additionalProperties( Schema $schema ): self { public function additionalProperties( Schema $schema ): self {
return $this->updateSchemaProperty( 'additionalProperties', $schema->toArray() ); return $this->update_schema_property( 'additionalProperties', $schema->to_array() );
} }
/**
* Disables additional properties.
*/
public function disableAdditionalProperties(): self { public function disableAdditionalProperties(): self {
return $this->updateSchemaProperty( 'additionalProperties', false ); return $this->update_schema_property( 'additionalProperties', false );
} }
/** /**
* Keys of $properties are regular expressions without leading/trailing delimiters. * Keys of $properties are regular expressions without leading/trailing delimiters.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#patternproperties * See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#patternproperties
* *
* @param array<string, Schema> $properties * @param array<string, Schema> $properties Regular expressions and their schemas.
*/ */
public function patternProperties( array $properties ): self { public function patternProperties( array $properties ): self {
$patternProperties = array(); $pattern_properties = array();
foreach ( $properties as $key => $value ) { foreach ( $properties as $key => $value ) {
$this->validatePattern( $key ); $this->validate_pattern( $key );
$patternProperties[ $key ] = $value->toArray(); $pattern_properties[ $key ] = $value->to_array();
} }
return $this->updateSchemaProperty( 'patternProperties', $patternProperties ); return $this->update_schema_property( 'patternProperties', $pattern_properties );
} }
/**
* Sets the minimum number of properties in the object.
*
* @param int $value Minimum number of properties in the object.
*/
public function minProperties( int $value ): self { public function minProperties( int $value ): self {
return $this->updateSchemaProperty( 'minProperties', $value ); return $this->update_schema_property( 'minProperties', $value );
} }
/**
* Sets the maximum number of properties in the object.
*
* @param int $value Maximum number of properties in the object.
*/
public function maxProperties( int $value ): self { public function maxProperties( int $value ): self {
return $this->updateSchemaProperty( 'maxProperties', $value ); return $this->update_schema_property( 'maxProperties', $value );
} }
} }

View File

@ -1,40 +1,64 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator\Schema; namespace MailPoet\EmailEditor\Validator\Schema;
use MailPoet\EmailEditor\Validator\Schema; use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#oneof-and-anyof /**
* Represents a schema that allows a value to match one of the given schemas.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#oneof-and-anyof
*/
class One_Of_Schema extends Schema { class One_Of_Schema extends Schema {
/**
* Schema definition.
*
* @var array
*/
protected $schema = array( protected $schema = array(
'oneOf' => array(), 'oneOf' => array(),
); );
/** @param Schema[] $schemas */ /**
* One_Of_Schema constructor.
*
* @param Schema[] $schemas List of schemas.
*/
public function __construct( public function __construct(
array $schemas array $schemas
) { ) {
foreach ( $schemas as $schema ) { foreach ( $schemas as $schema ) {
$this->schema['oneOf'][] = $schema->toArray(); $this->schema['oneOf'][] = $schema->to_array();
} }
} }
/**
* Sets the schema as nullable.
*/
public function nullable(): self { public function nullable(): self {
$null = array( 'type' => 'null' ); $null = array( 'type' => 'null' );
$oneOf = $this->schema['oneOf']; $one_of = $this->schema['oneOf'];
$value = in_array( $null, $oneOf, true ) ? $oneOf : array_merge( $oneOf, array( $null ) ); $value = in_array( $null, $one_of, true ) ? $one_of : array_merge( $one_of, array( $null ) );
return $this->updateSchemaProperty( 'oneOf', $value ); return $this->update_schema_property( 'oneOf', $value );
} }
public function nonNullable(): self { /**
$null = array( 'type' => 'null' ); * Sets the schema as non-nullable.
$oneOf = $this->schema['oneOf']; */
$value = array_filter( public function non_nullable(): self {
$oneOf, $null = array( 'type' => 'null' );
$one_of = $this->schema['one_of'];
$value = array_filter(
$one_of,
function ( $item ) use ( $null ) { function ( $item ) use ( $null ) {
return $item !== $null; return $item !== $null;
} }
); );
return $this->updateSchemaProperty( 'oneOf', $value ); return $this->update_schema_property( 'one_of', $value );
} }
} }

View File

@ -1,53 +1,97 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator\Schema; namespace MailPoet\EmailEditor\Validator\Schema;
use MailPoet\EmailEditor\Validator\Schema; use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#strings /**
* Represents a schema for a string.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#strings
*/
class String_Schema extends Schema { class String_Schema extends Schema {
/**
* Schema definition.
*
* @var array
*/
protected $schema = array( protected $schema = array(
'type' => 'string', 'type' => 'string',
); );
/**
* Set minimum length of the string.
*
* @param int $value Minimum length.
*/
public function minLength( int $value ): self { public function minLength( int $value ): self {
return $this->updateSchemaProperty( 'minLength', $value ); return $this->update_schema_property( 'minLength', $value );
} }
/**
* Set maximum length of the string.
*
* @param int $value Maximum length.
*/
public function maxLength( int $value ): self { public function maxLength( int $value ): self {
return $this->updateSchemaProperty( 'maxLength', $value ); return $this->update_schema_property( 'maxLength', $value );
} }
/** /**
* Parameter $pattern is a regular expression without leading/trailing delimiters. * Parameter $pattern is a regular expression without leading/trailing delimiters.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#pattern * See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#pattern
*
* @param string $pattern Regular expression pattern.
*/ */
public function pattern( string $pattern ): self { public function pattern( string $pattern ): self {
$this->validatePattern( $pattern ); $this->validate_pattern( $pattern );
return $this->updateSchemaProperty( 'pattern', $pattern ); return $this->update_schema_property( 'pattern', $pattern );
} }
/**
* Set the format of the string according to DateTime.
*/
public function formatDateTime(): self { public function formatDateTime(): self {
return $this->updateSchemaProperty( 'format', 'date-time' ); return $this->update_schema_property( 'format', 'date-time' );
} }
/**
* Set the format of the string according to email.
*/
public function formatEmail(): self { public function formatEmail(): self {
return $this->updateSchemaProperty( 'format', 'email' ); return $this->update_schema_property( 'format', 'email' );
} }
/**
* Set the format of the string according to Hex color.
*/
public function formatHexColor(): self { public function formatHexColor(): self {
return $this->updateSchemaProperty( 'format', 'hex-color' ); return $this->update_schema_property( 'format', 'hex-color' );
} }
/**
* Set the format of the string according to IP address.
*/
public function formatIp(): self { public function formatIp(): self {
return $this->updateSchemaProperty( 'format', 'ip' ); return $this->update_schema_property( 'format', 'ip' );
} }
/**
* Set the format of the string according to uri.
*/
public function formatUri(): self { public function formatUri(): self {
return $this->updateSchemaProperty( 'format', 'uri' ); return $this->update_schema_property( 'format', 'uri' );
} }
/**
* Set the format of the string according to uuid.
*/
public function formatUuid(): self { public function formatUuid(): self {
return $this->updateSchemaProperty( 'format', 'uuid' ); return $this->update_schema_property( 'format', 'uuid' );
} }
} }

View File

@ -1,5 +1,11 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator; namespace MailPoet\EmailEditor\Validator;
use MailPoet\EmailEditor\Validator\Schema\Any_Of_Schema; use MailPoet\EmailEditor\Validator\Schema\Any_Of_Schema;
@ -12,46 +18,81 @@ use MailPoet\EmailEditor\Validator\Schema\Object_Schema;
use MailPoet\EmailEditor\Validator\Schema\One_Of_Schema; use MailPoet\EmailEditor\Validator\Schema\One_Of_Schema;
use MailPoet\EmailEditor\Validator\Schema\String_Schema; use MailPoet\EmailEditor\Validator\Schema\String_Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/ /**
* Builder for creating schema objects.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/
*/
class Builder { class Builder {
/**
* Creates a schema for a string.
*/
public static function string(): String_Schema { public static function string(): String_Schema {
return new String_Schema(); return new String_Schema();
} }
/**
* Creates a schema for a number.
*/
public static function number(): Number_Schema { public static function number(): Number_Schema {
return new Number_Schema(); return new Number_Schema();
} }
/**
* Creates a schema for an integer.
*/
public static function integer(): Integer_Schema { public static function integer(): Integer_Schema {
return new Integer_Schema(); return new Integer_Schema();
} }
/**
* Creates a schema for a boolean.
*/
public static function boolean(): Boolean_Schema { public static function boolean(): Boolean_Schema {
return new Boolean_Schema(); return new Boolean_Schema();
} }
/**
* Creates a schema for null.
*/
public static function null(): Null_Schema { public static function null(): Null_Schema {
return new Null_Schema(); return new Null_Schema();
} }
/**
* Creates a schema for an array.
*
* @param Schema|null $items Schema of the items in the array.
*/
public static function array( Schema $items = null ): Array_Schema { public static function array( Schema $items = null ): Array_Schema {
$array = new Array_Schema(); $array = new Array_Schema();
return $items ? $array->items( $items ) : $array; return $items ? $array->items( $items ) : $array;
} }
/** @param array<string, Schema>|null $properties */ /**
* Creates a schema for an object.
*
* @param array<string, Schema>|null $properties Properties of the object.
*/
public static function object( array $properties = null ): Object_Schema { public static function object( array $properties = null ): Object_Schema {
$object = new Object_Schema(); $object = new Object_Schema();
return $properties === null ? $object : $object->properties( $properties ); return null === $properties ? $object : $object->properties( $properties );
} }
/** @param Schema[] $schemas */ /**
public static function oneOf( array $schemas ): One_Of_Schema { * Creates a schema that allows a value to match one of the given schemas.
*
* @param Schema[] $schemas List of schemas.
*/
public static function one_of( array $schemas ): One_Of_Schema {
return new One_Of_Schema( $schemas ); return new One_Of_Schema( $schemas );
} }
/** @param Schema[] $schemas */ /**
public static function anyOf( array $schemas ): Any_Of_Schema { * Creates a schema that allows a value to match any of the given schemas.
*
* @param Schema[] $schemas List of schemas.
*/
public static function any_of( array $schemas ): Any_Of_Schema {
return new Any_Of_Schema( $schemas ); return new Any_Of_Schema( $schemas );
} }
} }

View File

@ -1,96 +1,177 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator; namespace MailPoet\EmailEditor\Validator;
use function json_encode; use function wp_json_encode;
use function rest_get_allowed_schema_keywords; use function rest_get_allowed_schema_keywords;
/**
* Represents abastract schema.
*/
abstract class Schema { abstract class Schema {
/**
* Schema definition.
*
* @var array
*/
protected $schema = array(); protected $schema = array();
/** @return static */ /**
* Sets the schema as nullable.
*
* @return static
*/
public function nullable() { public function nullable() {
$type = $this->schema['type'] ?? array( 'null' ); $type = $this->schema['type'] ?? array( 'null' );
return $this->updateSchemaProperty( 'type', is_array( $type ) ? $type : array( $type, 'null' ) ); return $this->update_schema_property( 'type', is_array( $type ) ? $type : array( $type, 'null' ) );
} }
/** @return static */ /**
public function nonNullable() { * Sets the schema as non-nullable.
*
* @return static
*/
public function non_nullable() {
$type = $this->schema['type'] ?? null; $type = $this->schema['type'] ?? null;
return $type === null return null === $type
? $this->unsetSchemaProperty( 'type' ) ? $this->unset_schema_property( 'type' )
: $this->updateSchemaProperty( 'type', is_array( $type ) ? $type[0] : $type ); : $this->update_schema_property( 'type', is_array( $type ) ? $type[0] : $type );
} }
/** @return static */ /**
* Sets the schema as required.
*
* @return static
*/
public function required() { public function required() {
return $this->updateSchemaProperty( 'required', true ); return $this->update_schema_property( 'required', true );
} }
/** @return static */ /**
* Unsets the required property.
*
* @return static
*/
public function optional() { public function optional() {
return $this->unsetSchemaProperty( 'required' ); return $this->unset_schema_property( 'required' );
} }
/** @return static */ /**
* Set the title of the schema.
*
* @param string $title Title.
* @return static
*/
public function title( string $title ) { public function title( string $title ) {
return $this->updateSchemaProperty( 'title', $title ); return $this->update_schema_property( 'title', $title );
} }
/** @return static */ /**
* Set the description of the schema.
*
* @param string $description Description.
* @return static
*/
public function description( string $description ) { public function description( string $description ) {
return $this->updateSchemaProperty( 'description', $description ); return $this->update_schema_property( 'description', $description );
} }
/** @return static */ /**
public function default( $default ) { * Set the default value.
return $this->updateSchemaProperty( 'default', $default ); *
* @param mixed $default_value Default value.
* @return static
*/
public function default( $default_value ) {
return $this->update_schema_property( 'default', $default_value );
} }
/** @return static */ /**
* Set the field name and value.
*
* @param string $name Name of the field.
* @param mixed $value Value of the field.
* @return static
* @throws \Exception When the field name is reserved.
*/
public function field( string $name, $value ) { public function field( string $name, $value ) {
if ( in_array( $name, $this->getReservedKeywords(), true ) ) { if ( in_array( $name, $this->get_reserved_keywords(), true ) ) {
throw new \Exception( "Field name '$name' is reserved" ); throw new \Exception( \esc_html( "Field name '$name' is reserved" ) );
} }
return $this->updateSchemaProperty( $name, $value ); return $this->update_schema_property( $name, $value );
} }
public function toArray(): array { /**
* Returns the schema as an array.
*/
public function to_array(): array {
return $this->schema; return $this->schema;
} }
public function toString(): string { /**
$json = json_encode( $this->schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION ); * Returns the schema as a JSON string.
*
* @throws \Exception When the schema cannot be converted to JSON.
*/
public function to_string(): string {
$json = wp_json_encode( $this->schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION );
$error = json_last_error(); $error = json_last_error();
if ( $error || $json === false ) { if ( $error || false === $json ) {
throw new \Exception( json_last_error_msg(), (string) $error ); throw new \Exception( \esc_html( json_last_error_msg() ), \esc_html( (string) $error ) );
} }
return $json; return $json;
} }
/** @return static */ /**
protected function updateSchemaProperty( string $name, $value ) { * Updates the schema property.
*
* @param string $name Property name.
* @param mixed $value Property value.
* @return static
*/
protected function update_schema_property( string $name, $value ) {
$clone = clone $this; $clone = clone $this;
$clone->schema[ $name ] = $value; $clone->schema[ $name ] = $value;
return $clone; return $clone;
} }
/** @return static */ /**
protected function unsetSchemaProperty( string $name ) { * Unsets the schema property.
*
* @param string $name Property name.
*/
protected function unset_schema_property( string $name ) {
$clone = clone $this; $clone = clone $this;
unset( $clone->schema[ $name ] ); unset( $clone->schema[ $name ] );
return $clone; return $clone;
} }
protected function getReservedKeywords(): array { /**
* Returns reserved keywords.
*
* @return string[]
*/
protected function get_reserved_keywords(): array {
return rest_get_allowed_schema_keywords(); return rest_get_allowed_schema_keywords();
} }
protected function validatePattern( string $pattern ): void { /**
* Validates the regular expression pattern.
*
* @param string $pattern Regular expression pattern.
* @throws \Exception When the pattern is invalid.
*/
protected function validate_pattern( string $pattern ): void {
$escaped = str_replace( '#', '\\#', $pattern ); $escaped = str_replace( '#', '\\#', $pattern );
$regex = "#$escaped#u"; $regex = "#$escaped#u";
if ( @preg_match( $regex, '' ) === false ) { if ( @preg_match( $regex, '' ) === false ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
throw new \Exception( "Invalid regular expression '$regex'" ); throw new \Exception( \esc_html( "Invalid regular expression '$regex'" ) );
} }
} }
} }

View File

@ -1,22 +1,43 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator; namespace MailPoet\EmailEditor\Validator;
use MailPoet\UnexpectedValueException; use MailPoet\UnexpectedValueException;
use WP_Error; use WP_Error;
/**
* Exception thrown when validation fails.
*/
class Validation_Exception extends UnexpectedValueException { class Validation_Exception extends UnexpectedValueException {
/** @var WP_Error */ /**
protected $wpError; * WP_Error instance.
*
* @var WP_Error
*/
protected $wp_error;
public static function createFromWpError( WP_Error $wpError ): self { /**
$exception = self::create() * Creates a new instance of the exception.
->withMessage( $wpError->get_error_message() ); *
$exception->wpError = $wpError; * @param WP_Error $wp_error WP_Error instance.
*/
public static function create_from_wp_error( WP_Error $wp_error ): self {
$exception = self::create()
->withMessage( $wp_error->get_error_message() );
$exception->wp_error = $wp_error;
return $exception; return $exception;
} }
public function getWpError(): WP_Error { /**
return $this->wpError; * Returns the WP_Error instance.
*/
public function get_wp_error(): WP_Error {
return $this->wp_error;
} }
} }

View File

@ -1,5 +1,11 @@
<?php declare(strict_types = 1); <?php
/**
* This file is part of the MailPoet plugin.
*
* @package MailPoet\EmailEditor
*/
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator; namespace MailPoet\EmailEditor\Validator;
use JsonSerializable; use JsonSerializable;
@ -7,39 +13,37 @@ use MailPoet\WP\Functions as WPFunctions;
use stdClass; use stdClass;
use WP_Error; use WP_Error;
/**
* Validates and sanitizes values based on a schema.
*/
class Validator { class Validator {
/** @var WPFunctions */ /**
private $wp; * Strict validation & sanitization implementation.
* It only coerces int to float (e.g. 5 to 5.0).
public function __construct( *
WPFunctions $wp * @param Schema $schema The schema to validate against.
) { * @param mixed $value The value to validate.
$this->wp = $wp; * @param string $param_name The parameter name.
* @return mixed
*/
public function validate( Schema $schema, $value, string $param_name = 'value' ) {
return $this->validate_schema_array( $schema->to_array(), $value, $param_name );
} }
/** /**
* Strict validation & sanitization implementation. * Strict validation & sanitization implementation.
* It only coerces int to float (e.g. 5 to 5.0). * It only coerces int to float (e.g. 5 to 5.0).
* *
* @param mixed $value * @param array $schema The array must follow the format, which is returned from Schema::toArray().
* @param mixed $value The value to validate.
* @param string $param_name The parameter name.
* @return mixed * @return mixed
* @throws Validation_Exception If the value does not match the schema.
*/ */
public function validate( Schema $schema, $value, string $paramName = 'value' ) { public function validate_schema_array( array $schema, $value, string $param_name = 'value' ) {
return $this->validateSchemaArray( $schema->toArray(), $value, $paramName ); $result = $this->validate_and_sanitize_value_from_schema( $value, $schema, $param_name );
}
/**
* Strict validation & sanitization implementation.
* It only coerces int to float (e.g. 5 to 5.0).
*
* @param array $schema. The array must follow the format, which is returned from Schema::toArray().
* @param mixed $value
* @return mixed
*/
public function validateSchemaArray( array $schema, $value, string $paramName = 'value' ) {
$result = $this->validateAndSanitizeValueFromSchema( $value, $schema, $paramName );
if ( $result instanceof WP_Error ) { if ( $result instanceof WP_Error ) {
throw Validation_Exception::createFromWpError( $result ); throw Validation_Exception::create_from_wp_error( $result ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
} }
return $result; return $result;
} }
@ -47,51 +51,51 @@ class Validator {
/** /**
* Mirrors rest_validate_value_from_schema() and rest_sanitize_value_from_schema(). * Mirrors rest_validate_value_from_schema() and rest_sanitize_value_from_schema().
* *
* @param mixed $value * @param mixed $value The value to validate.
* @param array $schema * @param array $schema The schema to validate against.
* @param string $paramName * @param string $param_name The parameter name.
* @return mixed|WP_Error * @return mixed|WP_Error
*/ */
private function validateAndSanitizeValueFromSchema( $value, array $schema, string $paramName ) { private function validate_and_sanitize_value_from_schema( $value, array $schema, string $param_name ) {
// nullable // nullable.
$fullType = $schema['type'] ?? null; $full_type = $schema['type'] ?? null;
if ( is_array( $fullType ) && in_array( 'null', $fullType, true ) && $value === null ) { if ( is_array( $full_type ) && in_array( 'null', $full_type, true ) && null === $value ) {
return null; return null;
} }
// anyOf, oneOf // anyOf, oneOf.
if ( isset( $schema['anyOf'] ) ) { if ( isset( $schema['anyOf'] ) ) {
return $this->validateAndSanitizeAnyOf( $value, $schema, $paramName ); return $this->validate_and_sanitize_any_of( $value, $schema, $param_name );
} elseif ( isset( $schema['oneOf'] ) ) { } elseif ( isset( $schema['oneOf'] ) ) {
return $this->validateAndSanitizeOneOf( $value, $schema, $paramName ); return $this->validate_and_sanitize_one_of( $value, $schema, $param_name );
} }
// make types strict // make types strict.
$type = is_array( $fullType ) ? $fullType[0] : $fullType; $type = is_array( $full_type ) ? $full_type[0] : $full_type;
switch ( $type ) { switch ( $type ) {
case 'number': case 'number':
if ( ! is_float( $value ) && ! is_int( $value ) ) { if ( ! is_float( $value ) && ! is_int( $value ) ) {
return $this->getTypeError( $paramName, $fullType ); return $this->get_type_error( $param_name, $full_type );
} }
break; break;
case 'integer': case 'integer':
if ( ! is_int( $value ) ) { if ( ! is_int( $value ) ) {
return $this->getTypeError( $paramName, $fullType ); return $this->get_type_error( $param_name, $full_type );
} }
break; break;
case 'boolean': case 'boolean':
if ( ! is_bool( $value ) ) { if ( ! is_bool( $value ) ) {
return $this->getTypeError( $paramName, $fullType ); return $this->get_type_error( $param_name, $full_type );
} }
break; break;
case 'array': case 'array':
if ( ! is_array( $value ) ) { if ( ! is_array( $value ) ) {
return $this->getTypeError( $paramName, $fullType ); return $this->get_type_error( $param_name, $full_type );
} }
if ( isset( $schema['items'] ) ) { if ( isset( $schema['items'] ) ) {
foreach ( $value as $i => $v ) { foreach ( $value as $i => $v ) {
$result = $this->validateAndSanitizeValueFromSchema( $v, $schema['items'], $paramName . '[' . $i . ']' ); $result = $this->validate_and_sanitize_value_from_schema( $v, $schema['items'], $param_name . '[' . $i . ']' );
if ( $this->wp->isWpError( $result ) ) { if ( $this->wp->isWpError( $result ) ) {
return $result; return $result;
} }
@ -100,28 +104,28 @@ class Validator {
break; break;
case 'object': case 'object':
if ( ! is_array( $value ) && ! $value instanceof stdClass && ! $value instanceof JsonSerializable ) { if ( ! is_array( $value ) && ! $value instanceof stdClass && ! $value instanceof JsonSerializable ) {
return $this->getTypeError( $paramName, $fullType ); return $this->get_type_error( $param_name, $full_type );
} }
// ensure string keys // ensure string keys.
$value = (array) ( $value instanceof JsonSerializable ? $value->jsonSerialize() : $value ); $value = (array) ( $value instanceof JsonSerializable ? $value->jsonSerialize() : $value );
if ( count( array_filter( array_keys( $value ), 'is_string' ) ) !== count( $value ) ) { if ( count( array_filter( array_keys( $value ), 'is_string' ) ) !== count( $value ) ) {
return $this->getTypeError( $paramName, $fullType ); return $this->get_type_error( $param_name, $full_type );
} }
// validate object properties // validate object properties.
foreach ( $value as $k => $v ) { foreach ( $value as $k => $v ) {
if ( isset( $schema['properties'][ $k ] ) ) { if ( isset( $schema['properties'][ $k ] ) ) {
$result = $this->validateAndSanitizeValueFromSchema( $v, $schema['properties'][ $k ], $paramName . '[' . $k . ']' ); $result = $this->validate_and_sanitize_value_from_schema( $v, $schema['properties'][ $k ], $param_name . '[' . $k . ']' );
if ( $this->wp->isWpError( $result ) ) { if ( $this->wp->isWpError( $result ) ) {
return $result; return $result;
} }
continue; continue;
} }
$patternPropertySchema = $this->wp->restFindMatchingPatternPropertySchema( $k, $schema ); $pattern_property_schema = rest_find_matching_pattern_property_schema( $k, $schema );
if ( $patternPropertySchema ) { if ( $pattern_property_schema ) {
$result = $this->validateAndSanitizeValueFromSchema( $v, $patternPropertySchema, $paramName . '[' . $k . ']' ); $result = $this->validate_and_sanitize_value_from_schema( $v, $pattern_property_schema, $param_name . '[' . $k . ']' );
if ( $this->wp->isWpError( $result ) ) { if ( $this->wp->isWpError( $result ) ) {
return $result; return $result;
} }
@ -129,7 +133,7 @@ class Validator {
} }
if ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) { if ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) {
$result = $this->validateAndSanitizeValueFromSchema( $v, $schema['additionalProperties'], $paramName . '[' . $k . ']' ); $result = $this->validate_and_sanitize_value_from_schema( $v, $schema['additionalProperties'], $param_name . '[' . $k . ']' );
if ( $this->wp->isWpError( $result ) ) { if ( $this->wp->isWpError( $result ) ) {
return $result; return $result;
} }
@ -138,23 +142,25 @@ class Validator {
break; break;
} }
$result = $this->wp->restValidateValueFromSchema( $value, $schema, $paramName ); $result = rest_validate_value_from_schema( $value, $schema, $param_name );
if ( $this->wp->isWpError( $result ) ) { if ( is_wp_error( $result ) ) {
return $result; return $result;
} }
return $this->wp->restSanitizeValueFromSchema( $value, $schema, $paramName ); return rest_sanitize_value_from_schema( $value, $schema, $param_name );
} }
/** /**
* Mirrors rest_find_any_matching_schema(). * Mirrors rest_find_any_matching_schema().
* *
* @param mixed $value * @param mixed $value The value to validate.
* @param array $any_of_schema The schema to validate against.
* @param string $param_name The parameter name.
* @return mixed|WP_Error * @return mixed|WP_Error
*/ */
private function validateAndSanitizeAnyOf( $value, array $anyOfSchema, string $paramName ) { private function validate_and_sanitize_any_of( $value, array $any_of_schema, string $param_name ) {
$errors = array(); $errors = array();
foreach ( $anyOfSchema['anyOf'] as $index => $schema ) { foreach ( $any_of_schema['anyOf'] as $index => $schema ) {
$result = $this->validateAndSanitizeValueFromSchema( $value, $schema, $paramName ); $result = $this->validate_and_sanitize_value_from_schema( $value, $schema, $param_name );
if ( ! $this->wp->isWpError( $result ) ) { if ( ! $this->wp->isWpError( $result ) ) {
return $result; return $result;
} }
@ -164,21 +170,23 @@ class Validator {
'index' => $index, 'index' => $index,
); );
} }
return $this->wp->restGetCombiningOperationError( $value, $paramName, $errors ); return rest_get_combining_operation_error( $value, $param_name, $errors );
} }
/** /**
* Mirrors rest_find_one_matching_schema(). * Mirrors rest_find_one_matching_schema().
* *
* @param mixed $value * @param mixed $value The value to validate.
* @param array $one_of_schema The schema to validate against.
* @param string $param_name The parameter name.
* @return mixed|WP_Error * @return mixed|WP_Error
*/ */
private function validateAndSanitizeOneOf( $value, array $oneOfSchema, string $paramName ) { private function validate_and_sanitize_one_of( $value, array $one_of_schema, string $param_name ) {
$matchingSchemas = array(); $matching_schemas = array();
$errors = array(); $errors = array();
$data = null; $data = null;
foreach ( $oneOfSchema['oneOf'] as $index => $schema ) { foreach ( $one_of_schema['oneOf'] as $index => $schema ) {
$result = $this->validateAndSanitizeValueFromSchema( $value, $schema, $paramName ); $result = $this->validate_and_sanitize_value_from_schema( $value, $schema, $param_name );
if ( $this->wp->isWpError( $result ) ) { if ( $this->wp->isWpError( $result ) ) {
$errors[] = array( $errors[] = array(
'error_object' => $result, 'error_object' => $result,
@ -186,26 +194,31 @@ class Validator {
'index' => $index, 'index' => $index,
); );
} else { } else {
$data = $result; $data = $result;
$matchingSchemas[ $index ] = $schema; $matching_schemas[ $index ] = $schema;
} }
} }
if ( ! $matchingSchemas ) { if ( ! $matching_schemas ) {
return $this->wp->restGetCombiningOperationError( $value, $paramName, $errors ); return $this->wp->restGetCombiningOperationError( $value, $param_name, $errors );
} }
if ( count( $matchingSchemas ) > 1 ) { if ( count( $matching_schemas ) > 1 ) {
// reuse WP method to generate detailed error // reuse WP method to generate detailed error.
$invalidSchema = array( 'type' => array() ); $invalid_schema = array( 'type' => array() );
$oneOf = array_replace( array_fill( 0, count( $oneOfSchema['oneOf'] ), $invalidSchema ), $matchingSchemas ); $one_of = array_replace( array_fill( 0, count( $one_of_schema['oneOf'] ), $invalid_schema ), $matching_schemas );
return $this->wp->restFindOneMatchingSchema( $value, array( 'oneOf' => $oneOf ), $paramName ); return rest_find_one_matching_schema( $value, array( 'oneOf' => $one_of ), $param_name );
} }
return $data; return $data;
} }
/** @param string|string[] $type */ /**
private function getTypeError( string $param, $type ): WP_Error { * Returns a WP_Error for a type mismatch.
*
* @param string $param The parameter name.
* @param string|string[] $type The expected type.
*/
private function get_type_error( string $param, $type ): WP_Error {
$type = is_array( $type ) ? $type : array( $type ); $type = is_array( $type ) ? $type : array( $type );
return new WP_Error( return new WP_Error(
'rest_invalid_type', 'rest_invalid_type',