geno/wp-content/plugins/mailpoet/lib/Validator/Validator.php
2024-02-01 11:54:18 +00:00

212 lines
6.9 KiB
PHP

<?php declare(strict_types = 1);
namespace MailPoet\Validator;
if (!defined('ABSPATH')) exit;
use JsonSerializable;
use MailPoet\WP\Functions as WPFunctions;
use stdClass;
use WP_Error;
class Validator {
/** @var WPFunctions */
private $wp;
public function __construct(
WPFunctions $wp
) {
$this->wp = $wp;
}
/**
* Strict validation & sanitization implementation.
* It only coerces int to float (e.g. 5 to 5.0).
*
* @param mixed $value
* @return mixed
*/
public function validate(Schema $schema, $value, string $paramName = 'value') {
return $this->validateSchemaArray($schema->toArray(), $value, $paramName);
}
/**
* 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) {
throw ValidationException::createFromWpError($result);
}
return $result;
}
/**
* Mirrors rest_validate_value_from_schema() and rest_sanitize_value_from_schema().
*
* @param mixed $value
* @param array $schema
* @param string $paramName
* @return mixed|WP_Error
*/
private function validateAndSanitizeValueFromSchema($value, array $schema, string $paramName) {
// nullable
$fullType = $schema['type'] ?? null;
if (is_array($fullType) && in_array('null', $fullType, true) && $value === null) {
return null;
}
// anyOf, oneOf
if (isset($schema['anyOf'])) {
return $this->validateAndSanitizeAnyOf($value, $schema, $paramName);
} elseif (isset($schema['oneOf'])) {
return $this->validateAndSanitizeOneOf($value, $schema, $paramName);
}
// make types strict
$type = is_array($fullType) ? $fullType[0] : $fullType;
switch ($type) {
case 'number':
if (!is_float($value) && !is_int($value)) {
return $this->getTypeError($paramName, $fullType);
}
break;
case 'integer':
if (!is_int($value)) {
return $this->getTypeError($paramName, $fullType);
}
break;
case 'boolean':
if (!is_bool($value)) {
return $this->getTypeError($paramName, $fullType);
}
break;
case 'array':
if (!is_array($value)) {
return $this->getTypeError($paramName, $fullType);
}
if (isset($schema['items'])) {
foreach ($value as $i => $v) {
$result = $this->validateAndSanitizeValueFromSchema($v, $schema['items'], $paramName . '[' . $i . ']');
if ($this->wp->isWpError($result)) {
return $result;
}
}
}
break;
case 'object':
if (!is_array($value) && !$value instanceof stdClass && !$value instanceof JsonSerializable) {
return $this->getTypeError($paramName, $fullType);
}
// ensure string keys
$value = (array)($value instanceof JsonSerializable ? $value->jsonSerialize() : $value);
if (count(array_filter(array_keys($value), 'is_string')) !== count($value)) {
return $this->getTypeError($paramName, $fullType);
}
// validate object properties
foreach ($value as $k => $v) {
if (isset($schema['properties'][$k])) {
$result = $this->validateAndSanitizeValueFromSchema($v, $schema['properties'][$k], $paramName . '[' . $k . ']');
if ($this->wp->isWpError($result)) {
return $result;
}
continue;
}
$patternPropertySchema = $this->wp->restFindMatchingPatternPropertySchema($k, $schema);
if ($patternPropertySchema) {
$result = $this->validateAndSanitizeValueFromSchema($v, $patternPropertySchema, $paramName . '[' . $k . ']');
if ($this->wp->isWpError($result)) {
return $result;
}
continue;
}
if (isset($schema['additionalProperties']) && is_array($schema['additionalProperties'])) {
$result = $this->validateAndSanitizeValueFromSchema($v, $schema['additionalProperties'], $paramName . '[' . $k . ']');
if ($this->wp->isWpError($result)) {
return $result;
}
}
}
break;
}
$result = $this->wp->restValidateValueFromSchema($value, $schema, $paramName);
if ($this->wp->isWpError($result)) {
return $result;
}
return $this->wp->restSanitizeValueFromSchema($value, $schema, $paramName);
}
/**
* Mirrors rest_find_any_matching_schema().
*
* @param mixed $value
* @return mixed|WP_Error
*/
private function validateAndSanitizeAnyOf($value, array $anyOfSchema, string $paramName) {
$errors = [];
foreach ($anyOfSchema['anyOf'] as $index => $schema) {
$result = $this->validateAndSanitizeValueFromSchema($value, $schema, $paramName);
if (!$this->wp->isWpError($result)) {
return $result;
}
$errors[] = ['error_object' => $result, 'schema' => $schema, 'index' => $index];
}
return $this->wp->restGetCombiningOperationError($value, $paramName, $errors);
}
/**
* Mirrors rest_find_one_matching_schema().
*
* @param mixed $value
* @return mixed|WP_Error
*/
private function validateAndSanitizeOneOf($value, array $oneOfSchema, string $paramName) {
$matchingSchemas = [];
$errors = [];
$data = null;
foreach ($oneOfSchema['oneOf'] as $index => $schema) {
$result = $this->validateAndSanitizeValueFromSchema($value, $schema, $paramName);
if ($this->wp->isWpError($result)) {
$errors[] = ['error_object' => $result, 'schema' => $schema, 'index' => $index];
} else {
$data = $result;
$matchingSchemas[$index] = $schema;
}
}
if (!$matchingSchemas) {
return $this->wp->restGetCombiningOperationError($value, $paramName, $errors);
}
if (count($matchingSchemas) > 1) {
// reuse WP method to generate detailed error
$invalidSchema = ['type' => []];
$oneOf = array_replace(array_fill(0, count($oneOfSchema['oneOf']), $invalidSchema), $matchingSchemas);
return $this->wp->restFindOneMatchingSchema($value, ['oneOf' => $oneOf], $paramName);
}
return $data;
}
/** @param string|string[] $type */
private function getTypeError(string $param, $type): WP_Error {
$type = is_array($type) ? $type : [$type];
return new WP_Error(
'rest_invalid_type',
// translators: %1$s is the current parameter and %2$s a comma-separated list of the allowed types.
sprintf(__('%1$s is not of type %2$s.', 'mailpoet'), $param, implode(',', $type)),
['param' => $param]
);
}
}