Обновление клиента (apps, 3rdparty, install)

This commit is contained in:
root
2026-03-16 08:42:57 +00:00
parent b8905de237
commit f390426546
3354 changed files with 505213 additions and 3 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace JsonSchema;
use JsonSchema\Exception\InvalidArgumentException;
class ConstraintError extends Enum
{
public const ADDITIONAL_ITEMS = 'additionalItems';
public const ADDITIONAL_PROPERTIES = 'additionalProp';
public const ALL_OF = 'allOf';
public const ANY_OF = 'anyOf';
public const DEPENDENCIES = 'dependencies';
public const DISALLOW = 'disallow';
public const DIVISIBLE_BY = 'divisibleBy';
public const ENUM = 'enum';
public const CONSTANT = 'const';
public const EXCLUSIVE_MINIMUM = 'exclusiveMinimum';
public const EXCLUSIVE_MAXIMUM = 'exclusiveMaximum';
public const FORMAT_COLOR = 'colorFormat';
public const FORMAT_DATE = 'dateFormat';
public const FORMAT_DATE_TIME = 'dateTimeFormat';
public const FORMAT_DATE_UTC = 'dateUtcFormat';
public const FORMAT_EMAIL = 'emailFormat';
public const FORMAT_HOSTNAME = 'styleHostName';
public const FORMAT_IP = 'ipFormat';
public const FORMAT_PHONE = 'phoneFormat';
public const FORMAT_REGEX= 'regexFormat';
public const FORMAT_STYLE = 'styleFormat';
public const FORMAT_TIME = 'timeFormat';
public const FORMAT_URL = 'urlFormat';
public const FORMAT_URL_REF = 'urlRefFormat';
public const INVALID_SCHEMA = 'invalidSchema';
public const LENGTH_MAX = 'maxLength';
public const LENGTH_MIN = 'minLength';
public const MAXIMUM = 'maximum';
public const MIN_ITEMS = 'minItems';
public const MINIMUM = 'minimum';
public const MISSING_ERROR = 'missingError';
public const MISSING_MAXIMUM = 'missingMaximum';
public const MISSING_MINIMUM = 'missingMinimum';
public const MAX_ITEMS = 'maxItems';
public const MULTIPLE_OF = 'multipleOf';
public const NOT = 'not';
public const ONE_OF = 'oneOf';
public const REQUIRED = 'required';
public const REQUIRES = 'requires';
public const PATTERN = 'pattern';
public const PREGEX_INVALID = 'pregrex';
public const PROPERTIES_MIN = 'minProperties';
public const PROPERTIES_MAX = 'maxProperties';
public const TYPE = 'type';
public const UNIQUE_ITEMS = 'uniqueItems';
/**
* @return string
*/
public function getMessage()
{
$name = $this->getValue();
static $messages = [
self::ADDITIONAL_ITEMS => 'The item %s[%s] is not defined and the definition does not allow additional items',
self::ADDITIONAL_PROPERTIES => 'The property %s is not defined and the definition does not allow additional properties',
self::ALL_OF => 'Failed to match all schemas',
self::ANY_OF => 'Failed to match at least one schema',
self::DEPENDENCIES => '%s depends on %s, which is missing',
self::DISALLOW => 'Disallowed value was matched',
self::DIVISIBLE_BY => 'Is not divisible by %d',
self::ENUM => 'Does not have a value in the enumeration %s',
self::CONSTANT => 'Does not have a value equal to %s',
self::EXCLUSIVE_MINIMUM => 'Must have a minimum value greater than %d',
self::EXCLUSIVE_MAXIMUM => 'Must have a maximum value less than %d',
self::FORMAT_COLOR => 'Invalid color',
self::FORMAT_DATE => 'Invalid date %s, expected format YYYY-MM-DD',
self::FORMAT_DATE_TIME => 'Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm',
self::FORMAT_DATE_UTC => 'Invalid time %s, expected integer of milliseconds since Epoch',
self::FORMAT_EMAIL => 'Invalid email',
self::FORMAT_HOSTNAME => 'Invalid hostname',
self::FORMAT_IP => 'Invalid IP address',
self::FORMAT_PHONE => 'Invalid phone number',
self::FORMAT_REGEX=> 'Invalid regex format %s',
self::FORMAT_STYLE => 'Invalid style',
self::FORMAT_TIME => 'Invalid time %s, expected format hh:mm:ss',
self::FORMAT_URL => 'Invalid URL format',
self::FORMAT_URL_REF => 'Invalid URL reference format',
self::LENGTH_MAX => 'Must be at most %d characters long',
self::INVALID_SCHEMA => 'Schema is not valid',
self::LENGTH_MIN => 'Must be at least %d characters long',
self::MAX_ITEMS => 'There must be a maximum of %d items in the array, %d found',
self::MAXIMUM => 'Must have a maximum value less than or equal to %d',
self::MIN_ITEMS => 'There must be a minimum of %d items in the array, %d found',
self::MINIMUM => 'Must have a minimum value greater than or equal to %d',
self::MISSING_MAXIMUM => 'Use of exclusiveMaximum requires presence of maximum',
self::MISSING_MINIMUM => 'Use of exclusiveMinimum requires presence of minimum',
/*self::MISSING_ERROR => 'Used for tests; this error is deliberately commented out',*/
self::MULTIPLE_OF => 'Must be a multiple of %s',
self::NOT => 'Matched a schema which it should not',
self::ONE_OF => 'Failed to match exactly one schema',
self::REQUIRED => 'The property %s is required',
self::REQUIRES => 'The presence of the property %s requires that %s also be present',
self::PATTERN => 'Does not match the regex pattern %s',
self::PREGEX_INVALID => 'The pattern %s is invalid',
self::PROPERTIES_MIN => 'Must contain a minimum of %d properties',
self::PROPERTIES_MAX => 'Must contain no more than %d properties',
self::TYPE => '%s value found, but %s is required',
self::UNIQUE_ITEMS => 'There are no duplicates allowed in the array'
];
if (!isset($messages[$name])) {
throw new InvalidArgumentException('Missing error message for ' . $name);
}
return $messages[$name];
}
}
@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Constraints;
use const JSON_ERROR_NONE;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Exception\InvalidArgumentException;
use JsonSchema\Exception\ValidationException;
use JsonSchema\Validator;
/**
* A more basic constraint definition - used for the public
* interface to avoid exposing library internals.
*/
class BaseConstraint
{
/**
* @var array Errors
*/
protected $errors = [];
/**
* @var int All error types which have occurred
* @phpstan-var int-mask-of<Validator::ERROR_*>
*/
protected $errorMask = Validator::ERROR_NONE;
/**
* @var Factory
*/
protected $factory;
public function __construct(?Factory $factory = null)
{
$this->factory = $factory ?: new Factory();
}
public function addError(ConstraintError $constraint, ?JsonPointer $path = null, array $more = []): void
{
$message = $constraint->getMessage();
$name = $constraint->getValue();
$error = [
'property' => $this->convertJsonPointerIntoPropertyPath($path ?: new JsonPointer('')),
'pointer' => ltrim((string) ($path ?: new JsonPointer('')), '#'),
'message' => ucfirst(vsprintf($message, array_map(static function ($val) {
if (is_scalar($val)) {
return is_bool($val) ? var_export($val, true) : $val;
}
return json_encode($val);
}, array_values($more)))),
'constraint' => [
'name' => $name,
'params' => $more
],
'context' => $this->factory->getErrorContext(),
];
if ($this->factory->getConfig(Constraint::CHECK_MODE_EXCEPTIONS)) {
throw new ValidationException(sprintf('Error validating %s: %s', $error['pointer'], $error['message']));
}
$this->errors[] = $error;
$this->errorMask |= $error['context'];
}
public function addErrors(array $errors): void
{
if ($errors) {
$this->errors = array_merge($this->errors, $errors);
$errorMask = &$this->errorMask;
array_walk($errors, static function ($error) use (&$errorMask) {
if (isset($error['context'])) {
$errorMask |= $error['context'];
}
});
}
}
/**
* @phpstan-param int-mask-of<Validator::ERROR_*> $errorContext
*/
public function getErrors(int $errorContext = Validator::ERROR_ALL): array
{
if ($errorContext === Validator::ERROR_ALL) {
return $this->errors;
}
return array_filter($this->errors, static function ($error) use ($errorContext) {
return (bool) ($errorContext & $error['context']);
});
}
/**
* @phpstan-param int-mask-of<Validator::ERROR_*> $errorContext
*/
public function numErrors(int $errorContext = Validator::ERROR_ALL): int
{
if ($errorContext === Validator::ERROR_ALL) {
return count($this->errors);
}
return count($this->getErrors($errorContext));
}
public function isValid(): bool
{
return !$this->getErrors();
}
/**
* Clears any reported errors. Should be used between
* multiple validation checks.
*/
public function reset(): void
{
$this->errors = [];
$this->errorMask = Validator::ERROR_NONE;
}
/**
* Get the error mask
*
* @phpstan-return int-mask-of<Validator::ERROR_*>
*/
public function getErrorMask(): int
{
return $this->errorMask;
}
/**
* Recursively cast an associative array to an object
*/
public static function arrayToObjectRecursive(array $array): object
{
$json = json_encode($array);
if (json_last_error() !== JSON_ERROR_NONE) {
$message = 'Unable to encode schema array as JSON';
if (function_exists('json_last_error_msg')) {
$message .= ': ' . json_last_error_msg();
}
throw new InvalidArgumentException($message);
}
return (object) json_decode($json, false);
}
/**
* Transform a JSON pattern into a PCRE regex
*/
public static function jsonPatternToPhpRegex(string $pattern): string
{
return '~' . str_replace('~', '\\~', $pattern) . '~u';
}
protected function convertJsonPointerIntoPropertyPath(JsonPointer $pointer): string
{
$result = array_map(
static function ($path) {
return sprintf(is_numeric($path) ? '[%d]' : '.%s', $path);
},
$pointer->getPropertyPaths()
);
return trim(implode('', $result), '.');
}
}
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Constraints;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Tool\DeepComparer;
/**
* The CollectionConstraint Constraints, validates an array against a given schema
*
* @author Robert Schönthal <seroscho@googlemail.com>
* @author Bruno Prieto Reis <bruno.p.reis@gmail.com>
*/
class CollectionConstraint extends Constraint
{
/**
* {@inheritdoc}
*/
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
// Verify minItems
if (isset($schema->minItems) && count($value) < $schema->minItems) {
$this->addError(ConstraintError::MIN_ITEMS(), $path, ['minItems' => $schema->minItems, 'found' => count($value)]);
}
// Verify maxItems
if (isset($schema->maxItems) && count($value) > $schema->maxItems) {
$this->addError(ConstraintError::MAX_ITEMS(), $path, ['maxItems' => $schema->maxItems, 'found' => count($value)]);
}
// Verify uniqueItems
if (isset($schema->uniqueItems) && $schema->uniqueItems) {
$count = count($value);
for ($x = 0; $x < $count - 1; $x++) {
for ($y = $x + 1; $y < $count; $y++) {
if (DeepComparer::isEqual($value[$x], $value[$y])) {
$this->addError(ConstraintError::UNIQUE_ITEMS(), $path);
break 2;
}
}
}
}
$this->validateItems($value, $schema, $path, $i);
}
/**
* Validates the items
*
* @param array $value
* @param \stdClass $schema
* @param string $i
*/
protected function validateItems(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
if (\is_null($schema) || !isset($schema->items)) {
return;
}
if ($schema->items === true) {
return;
}
if (is_object($schema->items)) {
// just one type definition for the whole array
foreach ($value as $k => &$v) {
$initErrors = $this->getErrors();
// First check if its defined in "items"
$this->checkUndefined($v, $schema->items, $path, $k);
// Recheck with "additionalItems" if the first test fails
if (count($initErrors) < count($this->getErrors()) && (isset($schema->additionalItems) && $schema->additionalItems !== false)) {
$secondErrors = $this->getErrors();
$this->checkUndefined($v, $schema->additionalItems, $path, $k);
}
// Reset errors if needed
if (isset($secondErrors) && count($secondErrors) < count($this->getErrors())) {
$this->errors = $secondErrors;
} elseif (isset($secondErrors) && count($secondErrors) === count($this->getErrors())) {
$this->errors = $initErrors;
}
}
unset($v); /* remove dangling reference to prevent any future bugs
* caused by accidentally using $v elsewhere */
} else {
// Defined item type definitions
foreach ($value as $k => &$v) {
if (array_key_exists($k, $schema->items)) {
$this->checkUndefined($v, $schema->items[$k], $path, $k);
} else {
// Additional items
if (property_exists($schema, 'additionalItems')) {
if ($schema->additionalItems !== false) {
$this->checkUndefined($v, $schema->additionalItems, $path, $k);
} else {
$this->addError(
ConstraintError::ADDITIONAL_ITEMS(),
$path,
[
'item' => $i,
'property' => $k,
'additionalItems' => $schema->additionalItems
]
);
}
} else {
// Should be valid against an empty schema
$this->checkUndefined($v, new \stdClass(), $path, $k);
}
}
}
unset($v); /* remove dangling reference to prevent any future bugs
* caused by accidentally using $v elsewhere */
// Treat when we have more schema definitions than values, not for empty arrays
if (count($value) > 0) {
for ($k = count($value); $k < count($schema->items); $k++) {
$undefinedInstance = $this->factory->createInstanceFor('undefined');
$this->checkUndefined($undefinedInstance, $schema->items[$k], $path, $k);
}
}
}
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Constraints;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Tool\DeepComparer;
/**
* The ConstConstraint Constraints, validates an element against a constant value
*
* @author Martin Helmich <martin@helmich.me>
*/
class ConstConstraint extends Constraint
{
/**
* {@inheritdoc}
*/
public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void
{
// Only validate const if the attribute exists
if ($element instanceof UndefinedConstraint && (!isset($schema->required) || !$schema->required)) {
return;
}
$const = $schema->const;
$type = gettype($element);
$constType = gettype($const);
if ($this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) && $type === 'array' && $constType === 'object') {
if (DeepComparer::isEqual((object) $element, $const)) {
return;
}
}
if (DeepComparer::isEqual($element, $const)) {
return;
}
$this->addError(ConstraintError::CONSTANT(), $path, ['const' => $schema->const]);
}
}
@@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Constraints;
use JsonSchema\Entity\JsonPointer;
abstract class Constraint extends BaseConstraint implements ConstraintInterface
{
/** @var string */
protected $inlineSchemaProperty = '$schema';
public const CHECK_MODE_NONE = 0x00000000;
public const CHECK_MODE_NORMAL = 0x00000001;
public const CHECK_MODE_TYPE_CAST = 0x00000002;
public const CHECK_MODE_COERCE_TYPES = 0x00000004;
public const CHECK_MODE_APPLY_DEFAULTS = 0x00000008;
public const CHECK_MODE_EXCEPTIONS = 0x00000010;
public const CHECK_MODE_DISABLE_FORMAT = 0x00000020;
public const CHECK_MODE_EARLY_COERCE = 0x00000040;
public const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000080;
public const CHECK_MODE_VALIDATE_SCHEMA = 0x00000100;
/**
* Bubble down the path
*
* @param JsonPointer|null $path Current path
* @param mixed $i What to append to the path
*/
protected function incrementPath(?JsonPointer $path, $i): JsonPointer
{
$path = $path ?? new JsonPointer('');
if ($i === null || $i === '') {
return $path;
}
return $path->withPropertyPaths(
array_merge(
$path->getPropertyPaths(),
[$i]
)
);
}
/**
* Validates an array
*
* @param mixed $value
* @param mixed $schema
* @param mixed $i
*/
protected function checkArray(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
$validator = $this->factory->createInstanceFor('collection');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* Validates an object
*
* @param mixed $value
* @param mixed $schema
* @param mixed $properties
* @param mixed $additionalProperties
* @param mixed $patternProperties
* @param array<string> $appliedDefaults
*/
protected function checkObject(
&$value,
$schema = null,
?JsonPointer $path = null,
$properties = null,
$additionalProperties = null,
$patternProperties = null,
array $appliedDefaults = []
): void {
/** @var ObjectConstraint $validator */
$validator = $this->factory->createInstanceFor('object');
$validator->check($value, $schema, $path, $properties, $additionalProperties, $patternProperties, $appliedDefaults);
$this->addErrors($validator->getErrors());
}
/**
* Validates the type of the value
*
* @param mixed $value
* @param mixed $schema
* @param mixed $i
*/
protected function checkType(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
$validator = $this->factory->createInstanceFor('type');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* Checks a undefined element
*
* @param mixed $value
* @param mixed $schema
* @param mixed $i
*/
protected function checkUndefined(&$value, $schema = null, ?JsonPointer $path = null, $i = null, bool $fromDefault = false): void
{
/** @var UndefinedConstraint $validator */
$validator = $this->factory->createInstanceFor('undefined');
$validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i, $fromDefault);
$this->addErrors($validator->getErrors());
}
/**
* Checks a string element
*
* @param mixed $value
* @param mixed $schema
* @param mixed $i
*/
protected function checkString($value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
$validator = $this->factory->createInstanceFor('string');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* Checks a number element
*
* @param mixed $value
* @param mixed $schema
* @param mixed $i
*/
protected function checkNumber($value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
$validator = $this->factory->createInstanceFor('number');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* Checks a enum element
*
* @param mixed $value
* @param mixed $schema
* @param mixed $i
*/
protected function checkEnum($value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
$validator = $this->factory->createInstanceFor('enum');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* Checks a const element
*
* @param mixed $value
* @param mixed $schema
* @param mixed $i
*/
protected function checkConst($value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
$validator = $this->factory->createInstanceFor('const');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* Checks format of an element
*
* @param mixed $value
* @param mixed $schema
* @param mixed $i
*/
protected function checkFormat($value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
$validator = $this->factory->createInstanceFor('format');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* Get the type check based on the set check mode.
*/
protected function getTypeCheck(): TypeCheck\TypeCheckInterface
{
return $this->factory->getTypeCheck();
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Constraints;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
/**
* The Constraints Interface
*
* @author Robert Schönthal <seroscho@googlemail.com>
*/
interface ConstraintInterface
{
/**
* returns all collected errors
*/
public function getErrors(): array;
/**
* adds errors to this validator
*/
public function addErrors(array $errors): void;
/**
* adds an error
*
* @param ConstraintError $constraint the constraint/rule that is broken, e.g.: ConstraintErrors::LENGTH_MIN()
* @param array $more more array elements to add to the error
*/
public function addError(ConstraintError $constraint, ?JsonPointer $path = null, array $more = []): void;
/**
* checks if the validator has not raised errors
*/
public function isValid(): bool;
/**
* invokes the validation of an element
*
* @abstract
*
* @param mixed $value
* @param mixed $schema
* @param mixed $i
*
* @throws \JsonSchema\Exception\ExceptionInterface
*/
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void;
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Constraints;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Tool\DeepComparer;
/**
* The EnumConstraint Constraints, validates an element against a given set of possibilities
*
* @author Robert Schönthal <seroscho@googlemail.com>
* @author Bruno Prieto Reis <bruno.p.reis@gmail.com>
*/
class EnumConstraint extends Constraint
{
/**
* {@inheritdoc}
*/
public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void
{
// Only validate enum if the attribute exists
if ($element instanceof UndefinedConstraint && (!isset($schema->required) || !$schema->required)) {
return;
}
$type = gettype($element);
foreach ($schema->enum as $enum) {
$enumType = gettype($enum);
if ($enumType === 'object'
&& $type === 'array'
&& $this->factory->getConfig(self::CHECK_MODE_TYPE_CAST)
&& DeepComparer::isEqual((object) $element, $enum)
) {
return;
}
if (($type === $enumType) && DeepComparer::isEqual($element, $enum)) {
return;
}
if (is_numeric($element) && is_numeric($enum) && DeepComparer::isEqual((float) $element, (float) $enum)) {
return;
}
}
$this->addError(ConstraintError::ENUM(), $path, ['enum' => $schema->enum]);
}
}
@@ -0,0 +1,217 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Constraints;
use JsonSchema\Exception\InvalidArgumentException;
use JsonSchema\SchemaStorage;
use JsonSchema\SchemaStorageInterface;
use JsonSchema\Uri\UriRetriever;
use JsonSchema\UriRetrieverInterface;
use JsonSchema\Validator;
/**
* Factory for centralize constraint initialization.
*/
class Factory
{
/**
* @var SchemaStorageInterface
*/
protected $schemaStorage;
/**
* @var UriRetriever
*/
protected $uriRetriever;
/**
* @var int
* @phpstan-var int-mask-of<Constraint::CHECK_MODE_*>
*/
private $checkMode = Constraint::CHECK_MODE_NORMAL;
/**
* @var array<int, TypeCheck\TypeCheckInterface>
* @phpstan-var array<int-mask-of<Constraint::CHECK_MODE_*>, TypeCheck\TypeCheckInterface>
*/
private $typeCheck = [];
/**
* @var int Validation context
*/
protected $errorContext = Validator::ERROR_DOCUMENT_VALIDATION;
/**
* @var array
*/
protected $constraintMap = [
'array' => 'JsonSchema\Constraints\CollectionConstraint',
'collection' => 'JsonSchema\Constraints\CollectionConstraint',
'object' => 'JsonSchema\Constraints\ObjectConstraint',
'type' => 'JsonSchema\Constraints\TypeConstraint',
'undefined' => 'JsonSchema\Constraints\UndefinedConstraint',
'string' => 'JsonSchema\Constraints\StringConstraint',
'number' => 'JsonSchema\Constraints\NumberConstraint',
'enum' => 'JsonSchema\Constraints\EnumConstraint',
'const' => 'JsonSchema\Constraints\ConstConstraint',
'format' => 'JsonSchema\Constraints\FormatConstraint',
'schema' => 'JsonSchema\Constraints\SchemaConstraint',
'validator' => 'JsonSchema\Validator'
];
/**
* @var array<ConstraintInterface>
*/
private $instanceCache = [];
/**
* @phpstan-param int-mask-of<Constraint::CHECK_MODE_*> $checkMode
*/
public function __construct(
?SchemaStorageInterface $schemaStorage = null,
?UriRetrieverInterface $uriRetriever = null,
int $checkMode = Constraint::CHECK_MODE_NORMAL
) {
// set provided config options
$this->setConfig($checkMode);
$this->uriRetriever = $uriRetriever ?: new UriRetriever();
$this->schemaStorage = $schemaStorage ?: new SchemaStorage($this->uriRetriever);
}
/**
* Set config values
*
* @param int $checkMode Set checkMode options - does not preserve existing flags
* @phpstan-param int-mask-of<Constraint::CHECK_MODE_*> $checkMode
*/
public function setConfig(int $checkMode = Constraint::CHECK_MODE_NORMAL): void
{
$this->checkMode = $checkMode;
}
/**
* Enable checkMode flags
*
* @phpstan-param int-mask-of<Constraint::CHECK_MODE_*> $options
*/
public function addConfig(int $options): void
{
$this->checkMode |= $options;
}
/**
* Disable checkMode flags
*
* @phpstan-param int-mask-of<Constraint::CHECK_MODE_*> $options
*/
public function removeConfig(int $options): void
{
$this->checkMode &= ~$options;
}
/**
* Get checkMode option
*
* @param int|null $options Options to get, if null then return entire bitmask
* @phpstan-param int-mask-of<Constraint::CHECK_MODE_*>|null $options Options to get, if null then return entire bitmask
*
* @phpstan-return int-mask-of<Constraint::CHECK_MODE_*>
*/
public function getConfig(?int $options = null): int
{
if ($options === null) {
return $this->checkMode;
}
return $this->checkMode & $options;
}
public function getUriRetriever(): UriRetrieverInterface
{
return $this->uriRetriever;
}
public function getSchemaStorage(): SchemaStorageInterface
{
return $this->schemaStorage;
}
public function getTypeCheck(): TypeCheck\TypeCheckInterface
{
if (!isset($this->typeCheck[$this->checkMode])) {
$this->typeCheck[$this->checkMode] = ($this->checkMode & Constraint::CHECK_MODE_TYPE_CAST)
? new TypeCheck\LooseTypeCheck()
: new TypeCheck\StrictTypeCheck();
}
return $this->typeCheck[$this->checkMode];
}
public function setConstraintClass(string $name, string $class): Factory
{
// Ensure class exists
if (!class_exists($class)) {
throw new InvalidArgumentException('Unknown constraint ' . $name);
}
// Ensure class is appropriate
if (!in_array('JsonSchema\Constraints\ConstraintInterface', class_implements($class))) {
throw new InvalidArgumentException('Invalid class ' . $name);
}
$this->constraintMap[$name] = $class;
return $this;
}
/**
* Create a constraint instance for the given constraint name.
*
* @param string $constraintName
*
* @throws InvalidArgumentException if is not possible create the constraint instance
*
* @return ConstraintInterface&BaseConstraint
* @phpstan-return ConstraintInterface&BaseConstraint
*/
public function createInstanceFor($constraintName)
{
if (!isset($this->constraintMap[$constraintName])) {
throw new InvalidArgumentException('Unknown constraint ' . $constraintName);
}
if (!isset($this->instanceCache[$constraintName])) {
$this->instanceCache[$constraintName] = new $this->constraintMap[$constraintName]($this);
}
return clone $this->instanceCache[$constraintName];
}
/**
* Get the error context
*
* @phpstan-return Validator::ERROR_DOCUMENT_VALIDATION|Validator::ERROR_SCHEMA_VALIDATION
*/
public function getErrorContext(): int
{
return $this->errorContext;
}
/**
* Set the error context
*
* @phpstan-param Validator::ERROR_DOCUMENT_VALIDATION|Validator::ERROR_SCHEMA_VALIDATION $errorContext
*/
public function setErrorContext(int $errorContext): void
{
$this->errorContext = $errorContext;
}
}
@@ -0,0 +1,217 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Constraints;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Rfc3339;
use JsonSchema\Tool\Validator\RelativeReferenceValidator;
use JsonSchema\Tool\Validator\UriValidator;
/**
* Validates against the "format" property
*
* @author Justin Rainbow <justin.rainbow@gmail.com>
*
* @see http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.23
*/
class FormatConstraint extends Constraint
{
/**
* {@inheritdoc}
*/
public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void
{
if (!isset($schema->format) || $this->factory->getConfig(self::CHECK_MODE_DISABLE_FORMAT)) {
return;
}
switch ($schema->format) {
case 'date':
if (is_string($element) && !$date = $this->validateDateTime($element, 'Y-m-d')) {
$this->addError(ConstraintError::FORMAT_DATE(), $path, [
'date' => $element,
'format' => $schema->format
]
);
}
break;
case 'time':
if (is_string($element) && !$this->validateDateTime($element, 'H:i:s')) {
$this->addError(ConstraintError::FORMAT_TIME(), $path, [
'time' => json_encode($element),
'format' => $schema->format,
]
);
}
break;
case 'date-time':
if (is_string($element) && null === Rfc3339::createFromString($element)) {
$this->addError(ConstraintError::FORMAT_DATE_TIME(), $path, [
'dateTime' => json_encode($element),
'format' => $schema->format
]
);
}
break;
case 'utc-millisec':
if (!$this->validateDateTime($element, 'U')) {
$this->addError(ConstraintError::FORMAT_DATE_UTC(), $path, [
'value' => $element,
'format' => $schema->format]);
}
break;
case 'regex':
if (!$this->validateRegex($element)) {
$this->addError(ConstraintError::FORMAT_REGEX(), $path, [
'value' => $element,
'format' => $schema->format
]
);
}
break;
case 'color':
if (!$this->validateColor($element)) {
$this->addError(ConstraintError::FORMAT_COLOR(), $path, ['format' => $schema->format]);
}
break;
case 'style':
if (!$this->validateStyle($element)) {
$this->addError(ConstraintError::FORMAT_STYLE(), $path, ['format' => $schema->format]);
}
break;
case 'phone':
if (!$this->validatePhone($element)) {
$this->addError(ConstraintError::FORMAT_PHONE(), $path, ['format' => $schema->format]);
}
break;
case 'uri':
if (is_string($element) && !UriValidator::isValid($element)) {
$this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]);
}
break;
case 'uriref':
case 'uri-reference':
if (is_string($element) && !(UriValidator::isValid($element) || RelativeReferenceValidator::isValid($element))) {
$this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]);
}
break;
case 'email':
if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE | FILTER_FLAG_EMAIL_UNICODE)) {
$this->addError(ConstraintError::FORMAT_EMAIL(), $path, ['format' => $schema->format]);
}
break;
case 'ip-address':
case 'ipv4':
if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) {
$this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]);
}
break;
case 'ipv6':
if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) {
$this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]);
}
break;
case 'host-name':
case 'hostname':
if (!$this->validateHostname($element)) {
$this->addError(ConstraintError::FORMAT_HOSTNAME(), $path, ['format' => $schema->format]);
}
break;
default:
// Empty as it should be:
// The value of this keyword is called a format attribute. It MUST be a string.
// A format attribute can generally only validate a given set of instance types.
// If the type of the instance to validate is not in this set, validation for
// this format attribute and instance SHOULD succeed.
// http://json-schema.org/latest/json-schema-validation.html#anchor105
break;
}
}
protected function validateDateTime($datetime, $format)
{
$dt = \DateTime::createFromFormat($format, (string) $datetime);
if (!$dt) {
return false;
}
if ($datetime === $dt->format($format)) {
return true;
}
return false;
}
protected function validateRegex($regex)
{
if (!is_string($regex)) {
return true;
}
return false !== @preg_match(self::jsonPatternToPhpRegex($regex), '');
}
protected function validateColor($color)
{
if (!is_string($color)) {
return true;
}
if (in_array(strtolower($color), ['aqua', 'black', 'blue', 'fuchsia',
'gray', 'green', 'lime', 'maroon', 'navy', 'olive', 'orange', 'purple',
'red', 'silver', 'teal', 'white', 'yellow'])) {
return true;
}
return preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6})$/i', $color);
}
protected function validateStyle($style)
{
$properties = explode(';', rtrim($style, ';'));
$invalidEntries = preg_grep('/^\s*[-a-z]+\s*:\s*.+$/i', $properties, PREG_GREP_INVERT);
return empty($invalidEntries);
}
protected function validatePhone($phone)
{
return preg_match('/^\+?(\(\d{3}\)|\d{3}) \d{3} \d{4}$/', $phone);
}
protected function validateHostname($host)
{
if (!is_string($host)) {
return true;
}
$hostnameRegex = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/i';
return preg_match($hostnameRegex, $host);
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Constraints;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
/**
* The NumberConstraint Constraints, validates an number against a given schema
*
* @author Robert Schönthal <seroscho@googlemail.com>
* @author Bruno Prieto Reis <bruno.p.reis@gmail.com>
*/
class NumberConstraint extends Constraint
{
/**
* {@inheritdoc}
*/
public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void
{
// Verify minimum
if (isset($schema->exclusiveMinimum)) {
if (isset($schema->minimum)) {
if ($schema->exclusiveMinimum && $element <= $schema->minimum) {
$this->addError(ConstraintError::EXCLUSIVE_MINIMUM(), $path, ['minimum' => $schema->minimum]);
} elseif ($element < $schema->minimum) {
$this->addError(ConstraintError::MINIMUM(), $path, ['minimum' => $schema->minimum]);
}
} else {
$this->addError(ConstraintError::MISSING_MINIMUM(), $path);
}
} elseif (isset($schema->minimum) && $element < $schema->minimum) {
$this->addError(ConstraintError::MINIMUM(), $path, ['minimum' => $schema->minimum]);
}
// Verify maximum
if (isset($schema->exclusiveMaximum)) {
if (isset($schema->maximum)) {
if ($schema->exclusiveMaximum && $element >= $schema->maximum) {
$this->addError(ConstraintError::EXCLUSIVE_MAXIMUM(), $path, ['maximum' => $schema->maximum]);
} elseif ($element > $schema->maximum) {
$this->addError(ConstraintError::MAXIMUM(), $path, ['maximum' => $schema->maximum]);
}
} else {
$this->addError(ConstraintError::MISSING_MAXIMUM(), $path);
}
} elseif (isset($schema->maximum) && $element > $schema->maximum) {
$this->addError(ConstraintError::MAXIMUM(), $path, ['maximum' => $schema->maximum]);
}
// Verify divisibleBy - Draft v3
if (isset($schema->divisibleBy) && $this->fmod($element, $schema->divisibleBy) != 0) {
$this->addError(ConstraintError::DIVISIBLE_BY(), $path, ['divisibleBy' => $schema->divisibleBy]);
}
// Verify multipleOf - Draft v4
if (isset($schema->multipleOf) && $this->fmod($element, $schema->multipleOf) != 0) {
$this->addError(ConstraintError::MULTIPLE_OF(), $path, ['multipleOf' => $schema->multipleOf]);
}
$this->checkFormat($element, $schema, $path, $i);
}
private function fmod($number1, $number2)
{
$modulus = ($number1 - round($number1 / $number2) * $number2);
$precision = 0.0000000001;
if (-$precision < $modulus && $modulus < $precision) {
return 0.0;
}
return $modulus;
}
}
@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Constraints;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
class ObjectConstraint extends Constraint
{
/**
* @var list<string> List of properties to which a default value has been applied
*/
protected $appliedDefaults = [];
/**
* {@inheritdoc}
*
* @param list<string> $appliedDefaults
*/
public function check(
&$element,
$schema = null,
?JsonPointer $path = null,
$properties = null,
$additionalProp = null,
$patternProperties = null,
$appliedDefaults = []
): void {
if ($element instanceof UndefinedConstraint) {
return;
}
$this->appliedDefaults = $appliedDefaults;
$matches = [];
if ($patternProperties) {
// validate the element pattern properties
$matches = $this->validatePatternProperties($element, $path, $patternProperties);
}
if ($properties) {
// validate the element properties
$this->validateProperties($element, $properties, $path);
}
// validate additional element properties & constraints
$this->validateElement($element, $matches, $schema, $path, $properties, $additionalProp);
}
public function validatePatternProperties($element, ?JsonPointer $path, $patternProperties)
{
$matches = [];
foreach ($patternProperties as $pregex => $schema) {
$fullRegex = self::jsonPatternToPhpRegex($pregex);
// Validate the pattern before using it to test for matches
if (@preg_match($fullRegex, '') === false) {
$this->addError(ConstraintError::PREGEX_INVALID(), $path, ['pregex' => $pregex]);
continue;
}
foreach ($element as $i => $value) {
if (preg_match($fullRegex, $i)) {
$matches[] = $i;
$this->checkUndefined($value, $schema ?: new \stdClass(), $path, $i, in_array($i, $this->appliedDefaults));
}
}
}
return $matches;
}
/**
* Validates the element properties
*
* @param \StdClass $element Element to validate
* @param array $matches Matches from patternProperties (if any)
* @param \StdClass $schema ObjectConstraint definition
* @param JsonPointer|null $path Current test path
* @param \StdClass $properties Properties
* @param mixed $additionalProp Additional properties
*/
public function validateElement($element, $matches, $schema = null, ?JsonPointer $path = null,
$properties = null, $additionalProp = null)
{
$this->validateMinMaxConstraint($element, $schema, $path);
foreach ($element as $i => $value) {
$definition = $this->getProperty($properties, $i);
// no additional properties allowed
if (!in_array($i, $matches) && $additionalProp === false && $this->inlineSchemaProperty !== $i && !$definition) {
$this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $path, ['property' => $i]);
}
// additional properties defined
if (!in_array($i, $matches) && $additionalProp && !$definition) {
if ($additionalProp === true) {
$this->checkUndefined($value, null, $path, $i, in_array($i, $this->appliedDefaults));
} else {
$this->checkUndefined($value, $additionalProp, $path, $i, in_array($i, $this->appliedDefaults));
}
}
// property requires presence of another
$require = $this->getProperty($definition, 'requires');
if ($require && !$this->getProperty($element, $require)) {
$this->addError(ConstraintError::REQUIRES(), $path, [
'property' => $i,
'requiredProperty' => $require
]);
}
$property = $this->getProperty($element, $i, $this->factory->createInstanceFor('undefined'));
if (is_object($property)) {
$this->validateMinMaxConstraint(!($property instanceof UndefinedConstraint) ? $property : $element, $definition, $path);
}
}
}
/**
* Validates the definition properties
*
* @param \stdClass $element Element to validate
* @param \stdClass $properties Property definitions
* @param JsonPointer|null $path Path?
*/
public function validateProperties(&$element, $properties = null, ?JsonPointer $path = null)
{
$undefinedConstraint = $this->factory->createInstanceFor('undefined');
foreach ($properties as $i => $value) {
$property = &$this->getProperty($element, $i, $undefinedConstraint);
$definition = $this->getProperty($properties, $i);
if (is_object($definition)) {
// Undefined constraint will check for is_object() and quit if is not - so why pass it?
$this->checkUndefined($property, $definition, $path, $i, in_array($i, $this->appliedDefaults));
}
}
}
/**
* retrieves a property from an object or array
*
* @param mixed $element Element to validate
* @param string $property Property to retrieve
* @param mixed $fallback Default value if property is not found
*
* @return mixed
*/
protected function &getProperty(&$element, $property, $fallback = null)
{
if (is_array($element) && (isset($element[$property]) || array_key_exists($property, $element)) /*$this->checkMode == self::CHECK_MODE_TYPE_CAST*/) {
return $element[$property];
} elseif (is_object($element) && property_exists($element, (string) $property)) {
return $element->$property;
}
return $fallback;
}
/**
* validating minimum and maximum property constraints (if present) against an element
*
* @param \stdClass $element Element to validate
* @param \stdClass $objectDefinition ObjectConstraint definition
* @param JsonPointer|null $path Path to test?
*/
protected function validateMinMaxConstraint($element, $objectDefinition, ?JsonPointer $path = null)
{
if (!$this->getTypeCheck()::isObject($element)) {
return;
}
// Verify minimum number of properties
if (isset($objectDefinition->minProperties) && is_int($objectDefinition->minProperties)) {
if ($this->getTypeCheck()->propertyCount($element) < max(0, $objectDefinition->minProperties)) {
$this->addError(ConstraintError::PROPERTIES_MIN(), $path, ['minProperties' => $objectDefinition->minProperties]);
}
}
// Verify maximum number of properties
if (isset($objectDefinition->maxProperties) && is_int($objectDefinition->maxProperties)) {
if ($this->getTypeCheck()->propertyCount($element) > max(0, $objectDefinition->maxProperties)) {
$this->addError(ConstraintError::PROPERTIES_MAX(), $path, ['maxProperties' => $objectDefinition->maxProperties]);
}
}
}
}
@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Constraints;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Exception\InvalidArgumentException;
use JsonSchema\Exception\InvalidSchemaException;
use JsonSchema\Exception\RuntimeException;
use JsonSchema\Validator;
/**
* The SchemaConstraint Constraints, validates an element against a given schema
*
* @author Robert Schönthal <seroscho@googlemail.com>
* @author Bruno Prieto Reis <bruno.p.reis@gmail.com>
*/
class SchemaConstraint extends Constraint
{
private const DEFAULT_SCHEMA_SPEC = 'http://json-schema.org/draft-04/schema#';
/**
* {@inheritdoc}
*/
public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void
{
if ($schema !== null) {
// passed schema
$validationSchema = $schema;
} elseif ($this->getTypeCheck()->propertyExists($element, $this->inlineSchemaProperty)) {
// inline schema
$validationSchema = $this->getTypeCheck()->propertyGet($element, $this->inlineSchemaProperty);
} else {
throw new InvalidArgumentException('no schema found to verify against');
}
// cast array schemas to object
if (is_array($validationSchema)) {
$validationSchema = BaseConstraint::arrayToObjectRecursive($validationSchema);
}
// validate schema against whatever is defined in $validationSchema->$schema. If no
// schema is defined, assume self::DEFAULT_SCHEMA_SPEC (currently draft-04).
if ($this->factory->getConfig(self::CHECK_MODE_VALIDATE_SCHEMA)) {
if (!$this->getTypeCheck()->isObject($validationSchema)) {
throw new RuntimeException('Cannot validate the schema of a non-object');
}
if ($this->getTypeCheck()->propertyExists($validationSchema, '$schema')) {
$schemaSpec = $this->getTypeCheck()->propertyGet($validationSchema, '$schema');
} else {
$schemaSpec = self::DEFAULT_SCHEMA_SPEC;
}
// get the spec schema
$schemaStorage = $this->factory->getSchemaStorage();
if (!$this->getTypeCheck()->isObject($schemaSpec)) {
$schemaSpec = $schemaStorage->getSchema($schemaSpec);
}
// save error count, config & subtract CHECK_MODE_VALIDATE_SCHEMA
$initialErrorCount = $this->numErrors();
$initialConfig = $this->factory->getConfig();
$initialContext = $this->factory->getErrorContext();
$this->factory->removeConfig(self::CHECK_MODE_VALIDATE_SCHEMA | self::CHECK_MODE_APPLY_DEFAULTS);
$this->factory->addConfig(self::CHECK_MODE_TYPE_CAST);
$this->factory->setErrorContext(Validator::ERROR_SCHEMA_VALIDATION);
// validate schema
try {
$this->check($validationSchema, $schemaSpec);
} catch (\Exception $e) {
if ($this->factory->getConfig(self::CHECK_MODE_EXCEPTIONS)) {
throw new InvalidSchemaException('Schema did not pass validation', 0, $e);
}
}
if ($this->numErrors() > $initialErrorCount) {
$this->addError(ConstraintError::INVALID_SCHEMA(), $path);
}
// restore the initial config
$this->factory->setConfig($initialConfig);
$this->factory->setErrorContext($initialContext);
}
// validate element against $validationSchema
$this->checkUndefined($element, $validationSchema, $path, $i);
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Constraints;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
/**
* The StringConstraint Constraints, validates an string against a given schema
*
* @author Robert Schönthal <seroscho@googlemail.com>
* @author Bruno Prieto Reis <bruno.p.reis@gmail.com>
*/
class StringConstraint extends Constraint
{
/**
* {@inheritdoc}
*/
public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void
{
// Verify maxLength
if (isset($schema->maxLength) && $this->strlen($element) > $schema->maxLength) {
$this->addError(ConstraintError::LENGTH_MAX(), $path, [
'maxLength' => $schema->maxLength,
]);
}
//verify minLength
if (isset($schema->minLength) && $this->strlen($element) < $schema->minLength) {
$this->addError(ConstraintError::LENGTH_MIN(), $path, [
'minLength' => $schema->minLength,
]);
}
// Verify a regex pattern
if (isset($schema->pattern) && !preg_match(self::jsonPatternToPhpRegex($schema->pattern), $element)) {
$this->addError(ConstraintError::PATTERN(), $path, [
'pattern' => $schema->pattern,
]);
}
$this->checkFormat($element, $schema, $path, $i);
}
private function strlen($string)
{
if (extension_loaded('mbstring')) {
return mb_strlen($string, mb_detect_encoding($string));
}
// mbstring is present on all test platforms, so strlen() can be ignored for coverage
return strlen($string); // @codeCoverageIgnore
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Constraints\TypeCheck;
class LooseTypeCheck implements TypeCheckInterface
{
public static function isObject($value)
{
return
is_object($value) ||
(is_array($value) && (count($value) == 0 || self::isAssociativeArray($value)));
}
public static function isArray($value)
{
return
is_array($value) &&
(count($value) == 0 || !self::isAssociativeArray($value));
}
public static function propertyGet($value, $property)
{
if (is_object($value)) {
return $value->{$property};
}
return $value[$property];
}
public static function propertySet(&$value, $property, $data)
{
if (is_object($value)) {
$value->{$property} = $data;
} else {
$value[$property] = $data;
}
}
public static function propertyExists($value, $property)
{
if (is_object($value)) {
return property_exists($value, $property);
}
return is_array($value) && array_key_exists($property, $value);
}
public static function propertyCount($value)
{
if (is_object($value)) {
return count(get_object_vars($value));
}
return count($value);
}
/**
* Check if the provided array is associative or not
*
* @param array $arr
*
* @return bool
*/
private static function isAssociativeArray($arr)
{
return array_keys($arr) !== range(0, count($arr) - 1);
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Constraints\TypeCheck;
class StrictTypeCheck implements TypeCheckInterface
{
public static function isObject($value)
{
return is_object($value);
}
public static function isArray($value)
{
return is_array($value);
}
public static function propertyGet($value, $property)
{
return $value->{$property};
}
public static function propertySet(&$value, $property, $data)
{
$value->{$property} = $data;
}
public static function propertyExists($value, $property)
{
return property_exists($value, $property);
}
public static function propertyCount($value)
{
if (!is_object($value)) {
return 0;
}
return count(get_object_vars($value));
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Constraints\TypeCheck;
interface TypeCheckInterface
{
public static function isObject($value);
public static function isArray($value);
public static function propertyGet($value, $property);
public static function propertySet(&$value, $property, $data);
public static function propertyExists($value, $property);
public static function propertyCount($value);
}
@@ -0,0 +1,365 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Constraints;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Exception\InvalidArgumentException;
use UnexpectedValueException as StandardUnexpectedValueException;
/**
* The TypeConstraint Constraints, validates an element against a given type
*
* @author Robert Schönthal <seroscho@googlemail.com>
* @author Bruno Prieto Reis <bruno.p.reis@gmail.com>
*/
class TypeConstraint extends Constraint
{
/**
* @var array|string[] type wordings for validation error messages
*/
public static $wording = [
'integer' => 'an integer',
'number' => 'a number',
'boolean' => 'a boolean',
'object' => 'an object',
'array' => 'an array',
'string' => 'a string',
'null' => 'a null',
'any' => null, // validation of 'any' is always true so is not needed in message wording
0 => null, // validation of a false-y value is always true, so not needed as well
];
/**
* {@inheritdoc}
*/
public function check(&$value = null, $schema = null, ?JsonPointer $path = null, $i = null): void
{
$type = isset($schema->type) ? $schema->type : null;
$isValid = false;
$coerce = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES);
$earlyCoerce = $this->factory->getConfig(self::CHECK_MODE_EARLY_COERCE);
$wording = [];
if (is_array($type)) {
$this->validateTypesArray($value, $type, $wording, $isValid, $path, $coerce && $earlyCoerce);
if (!$isValid && $coerce && !$earlyCoerce) {
$this->validateTypesArray($value, $type, $wording, $isValid, $path, true);
}
} elseif (is_object($type)) {
$this->checkUndefined($value, $type, $path);
return;
} else {
$isValid = $this->validateType($value, $type, $coerce && $earlyCoerce);
if (!$isValid && $coerce && !$earlyCoerce) {
$isValid = $this->validateType($value, $type, true);
}
}
if ($isValid === false) {
if (!is_array($type)) {
$this->validateTypeNameWording($type);
$wording[] = self::$wording[$type];
}
$this->addError(ConstraintError::TYPE(), $path, [
'found' => gettype($value),
'expected' => $this->implodeWith($wording, ', ', 'or')
]);
}
}
/**
* Validates the given $value against the array of types in $type. Sets the value
* of $isValid to true, if at least one $type mateches the type of $value or the value
* passed as $isValid is already true.
*
* @param mixed $value Value to validate
* @param array $type TypeConstraints to check against
* @param array $validTypesWording An array of wordings of the valid types of the array $type
* @param bool $isValid The current validation value
* @param ?JsonPointer $path
* @param bool $coerce
*/
protected function validateTypesArray(&$value, array $type, &$validTypesWording, &$isValid, $path, $coerce = false)
{
foreach ($type as $tp) {
// already valid, so no need to waste cycles looping over everything
if ($isValid) {
return;
}
// $tp can be an object, if it's a schema instead of a simple type, validate it
// with a new type constraint
if (is_object($tp)) {
if (!$isValid) {
$validator = $this->factory->createInstanceFor('type');
$subSchema = new \stdClass();
$subSchema->type = $tp;
$validator->check($value, $subSchema, $path, null);
$error = $validator->getErrors();
$isValid = !(bool) $error;
$validTypesWording[] = self::$wording['object'];
}
} else {
$this->validateTypeNameWording($tp);
$validTypesWording[] = self::$wording[$tp];
if (!$isValid) {
$isValid = $this->validateType($value, $tp, $coerce);
}
}
}
}
/**
* Implodes the given array like implode() with turned around parameters and with the
* difference, that, if $listEnd isn't false, the last element delimiter is $listEnd instead of
* $delimiter.
*
* @param array $elements The elements to implode
* @param string $delimiter The delimiter to use
* @param bool $listEnd The last delimiter to use (defaults to $delimiter)
*
* @return string
*/
protected function implodeWith(array $elements, $delimiter = ', ', $listEnd = false)
{
if ($listEnd === false || !isset($elements[1])) {
return implode($delimiter, $elements);
}
$lastElement = array_slice($elements, -1);
$firsElements = join($delimiter, array_slice($elements, 0, -1));
$implodedElements = array_merge([$firsElements], $lastElement);
return join(" $listEnd ", $implodedElements);
}
/**
* Validates the given $type, if there's an associated self::$wording. If not, throws an
* exception.
*
* @param string $type The type to validate
*
* @throws StandardUnexpectedValueException
*/
protected function validateTypeNameWording($type)
{
if (!array_key_exists($type, self::$wording)) {
throw new StandardUnexpectedValueException(
sprintf(
'No wording for %s available, expected wordings are: [%s]',
var_export($type, true),
implode(', ', array_filter(self::$wording)))
);
}
}
/**
* Verifies that a given value is of a certain type
*
* @param mixed $value Value to validate
* @param string $type TypeConstraint to check against
*
* @throws InvalidArgumentException
*
* @return bool
*/
protected function validateType(&$value, $type, $coerce = false)
{
//mostly the case for inline schema
if (!$type) {
return true;
}
if ('any' === $type) {
return true;
}
if ('object' === $type) {
return $this->getTypeCheck()->isObject($value);
}
if ('array' === $type) {
if ($coerce) {
$value = $this->toArray($value);
}
return $this->getTypeCheck()->isArray($value);
}
if ('integer' === $type) {
if ($coerce) {
$value = $this->toInteger($value);
}
return is_int($value);
}
if ('number' === $type) {
if ($coerce) {
$value = $this->toNumber($value);
}
return is_numeric($value) && !is_string($value);
}
if ('boolean' === $type) {
if ($coerce) {
$value = $this->toBoolean($value);
}
return is_bool($value);
}
if ('string' === $type) {
if ($coerce) {
$value = $this->toString($value);
}
return is_string($value);
}
if ('null' === $type) {
if ($coerce) {
$value = $this->toNull($value);
}
return is_null($value);
}
throw new InvalidArgumentException((is_object($value) ? 'object' : $value) . ' is an invalid type for ' . $type);
}
/**
* Converts a value to boolean. For example, "true" becomes true.
*
* @param mixed $value The value to convert to boolean
*
* @return bool|mixed
*/
protected function toBoolean($value)
{
if ($value === 1 || $value === 'true') {
return true;
}
if (is_null($value) || $value === 0 || $value === 'false') {
return false;
}
if ($this->getTypeCheck()->isArray($value) && count($value) === 1) {
return $this->toBoolean(reset($value));
}
return $value;
}
/**
* Converts a value to a number. For example, "4.5" becomes 4.5.
*
* @param mixed $value the value to convert to a number
*
* @return int|float|mixed
*/
protected function toNumber($value)
{
if (is_numeric($value)) {
return $value + 0; // cast to number
}
if (is_bool($value) || is_null($value)) {
return (int) $value;
}
if ($this->getTypeCheck()->isArray($value) && count($value) === 1) {
return $this->toNumber(reset($value));
}
return $value;
}
/**
* Converts a value to an integer. For example, "4" becomes 4.
*
* @param mixed $value
*
* @return int|mixed
*/
protected function toInteger($value)
{
$numberValue = $this->toNumber($value);
if (is_numeric($numberValue) && (int) $numberValue == $numberValue) {
return (int) $numberValue; // cast to number
}
return $value;
}
/**
* Converts a value to an array containing that value. For example, [4] becomes 4.
*
* @param mixed $value
*
* @return array|mixed
*/
protected function toArray($value)
{
if (is_scalar($value) || is_null($value)) {
return [$value];
}
return $value;
}
/**
* Convert a value to a string representation of that value. For example, null becomes "".
*
* @param mixed $value
*
* @return string|mixed
*/
protected function toString($value)
{
if (is_numeric($value)) {
return "$value";
}
if ($value === true) {
return 'true';
}
if ($value === false) {
return 'false';
}
if (is_null($value)) {
return '';
}
if ($this->getTypeCheck()->isArray($value) && count($value) === 1) {
return $this->toString(reset($value));
}
return $value;
}
/**
* Convert a value to a null. For example, 0 becomes null.
*
* @param mixed $value
*
* @return null|mixed
*/
protected function toNull($value)
{
if ($value === 0 || $value === false || $value === '') {
return null;
}
if ($this->getTypeCheck()->isArray($value) && count($value) === 1) {
return $this->toNull(reset($value));
}
return $value;
}
}
@@ -0,0 +1,428 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Constraints;
use JsonSchema\ConstraintError;
use JsonSchema\Constraints\TypeCheck\LooseTypeCheck;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Exception\ValidationException;
use JsonSchema\Tool\DeepCopy;
use JsonSchema\Uri\UriResolver;
#[\AllowDynamicProperties]
class UndefinedConstraint extends Constraint
{
/**
* @var list<string> List of properties to which a default value has been applied
*/
protected $appliedDefaults = [];
/**
* {@inheritdoc}
* */
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null, bool $fromDefault = false): void
{
if (is_null($schema) || !is_object($schema)) {
return;
}
$path = $this->incrementPath($path, $i);
if ($fromDefault) {
$path->setFromDefault();
}
// check special properties
$this->validateCommonProperties($value, $schema, $path, $i);
// check allOf, anyOf, and oneOf properties
$this->validateOfProperties($value, $schema, $path, '');
// check known types
$this->validateTypes($value, $schema, $path, $i);
}
/**
* Validates the value against the types
*
* @param mixed $value
* @param mixed $schema
* @param JsonPointer $path
* @param string $i
*/
public function validateTypes(&$value, $schema, JsonPointer $path, $i = null)
{
// check array
if ($this->getTypeCheck()->isArray($value)) {
$this->checkArray($value, $schema, $path, $i);
}
// check object
if (LooseTypeCheck::isObject($value)) {
// object processing should always be run on assoc arrays,
// so use LooseTypeCheck here even if CHECK_MODE_TYPE_CAST
// is not set (i.e. don't use $this->getTypeCheck() here).
$this->checkObject(
$value,
$schema,
$path,
isset($schema->properties) ? $schema->properties : null,
isset($schema->additionalProperties) ? $schema->additionalProperties : null,
isset($schema->patternProperties) ? $schema->patternProperties : null,
$this->appliedDefaults
);
}
// check string
if (is_string($value)) {
$this->checkString($value, $schema, $path, $i);
}
// check numeric
if (is_numeric($value)) {
$this->checkNumber($value, $schema, $path, $i);
}
// check enum
if (isset($schema->enum)) {
$this->checkEnum($value, $schema, $path, $i);
}
// check const
if (isset($schema->const)) {
$this->checkConst($value, $schema, $path, $i);
}
}
/**
* Validates common properties
*
* @param mixed $value
* @param mixed $schema
* @param JsonPointer $path
* @param string $i
*/
protected function validateCommonProperties(&$value, $schema, JsonPointer $path, $i = '')
{
// if it extends another schema, it must pass that schema as well
if (isset($schema->extends)) {
if (is_string($schema->extends)) {
$schema->extends = $this->validateUri($schema, $schema->extends);
}
if (is_array($schema->extends)) {
foreach ($schema->extends as $extends) {
$this->checkUndefined($value, $extends, $path, $i);
}
} else {
$this->checkUndefined($value, $schema->extends, $path, $i);
}
}
// Apply default values from schema
if (!$path->fromDefault()) {
$this->applyDefaultValues($value, $schema, $path);
}
// Verify required values
if ($this->getTypeCheck()->isObject($value)) {
if (!($value instanceof self) && isset($schema->required) && is_array($schema->required)) {
// Draft 4 - Required is an array of strings - e.g. "required": ["foo", ...]
foreach ($schema->required as $required) {
if (!$this->getTypeCheck()->propertyExists($value, $required)) {
$this->addError(
ConstraintError::REQUIRED(),
$this->incrementPath($path, $required), [
'property' => $required
]
);
}
}
} elseif (isset($schema->required) && !is_array($schema->required)) {
// Draft 3 - Required attribute - e.g. "foo": {"type": "string", "required": true}
if ($schema->required && $value instanceof self) {
$propertyPaths = $path->getPropertyPaths();
$propertyName = end($propertyPaths);
$this->addError(ConstraintError::REQUIRED(), $path, ['property' => $propertyName]);
}
} else {
// if the value is both undefined and not required, skip remaining checks
// in this method which assume an actual, defined instance when validating.
if ($value instanceof self) {
return;
}
}
}
// Verify type
if (!($value instanceof self)) {
$this->checkType($value, $schema, $path, $i);
}
// Verify disallowed items
if (isset($schema->disallow)) {
$initErrors = $this->getErrors();
$typeSchema = new \stdClass();
$typeSchema->type = $schema->disallow;
$this->checkType($value, $typeSchema, $path);
// if no new errors were raised it must be a disallowed value
if (count($this->getErrors()) == count($initErrors)) {
$this->addError(ConstraintError::DISALLOW(), $path);
} else {
$this->errors = $initErrors;
}
}
if (isset($schema->not)) {
$initErrors = $this->getErrors();
$this->checkUndefined($value, $schema->not, $path, $i);
// if no new errors were raised then the instance validated against the "not" schema
if (count($this->getErrors()) == count($initErrors)) {
$this->addError(ConstraintError::NOT(), $path);
} else {
$this->errors = $initErrors;
}
}
// Verify that dependencies are met
if (isset($schema->dependencies) && $this->getTypeCheck()->isObject($value)) {
$this->validateDependencies($value, $schema->dependencies, $path);
}
}
/**
* Check whether a default should be applied for this value
*
* @param mixed $schema
* @param mixed $parentSchema
* @param bool $requiredOnly
*
* @return bool
*/
private function shouldApplyDefaultValue($requiredOnly, $schema, $name = null, $parentSchema = null)
{
// required-only mode is off
if (!$requiredOnly) {
return true;
}
// draft-04 required is set
if (
$name !== null
&& isset($parentSchema->required)
&& is_array($parentSchema->required)
&& in_array($name, $parentSchema->required)
) {
return true;
}
// draft-03 required is set
if (isset($schema->required) && !is_array($schema->required) && $schema->required) {
return true;
}
// default case
return false;
}
/**
* Apply default values
*
* @param mixed $value
* @param mixed $schema
* @param JsonPointer $path
*/
protected function applyDefaultValues(&$value, $schema, $path): void
{
// only apply defaults if feature is enabled
if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) {
return;
}
// apply defaults if appropriate
$requiredOnly = (bool) $this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS);
if (isset($schema->properties) && LooseTypeCheck::isObject($value)) {
// $value is an object or assoc array, and properties are defined - treat as an object
foreach ($schema->properties as $currentProperty => $propertyDefinition) {
$propertyDefinition = $this->factory->getSchemaStorage()->resolveRefSchema($propertyDefinition);
if (
!LooseTypeCheck::propertyExists($value, $currentProperty)
&& property_exists($propertyDefinition, 'default')
&& $this->shouldApplyDefaultValue($requiredOnly, $propertyDefinition, $currentProperty, $schema)
) {
// assign default value
if (is_object($propertyDefinition->default)) {
LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default);
} else {
LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default);
}
$this->appliedDefaults[] = $currentProperty;
}
}
} elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) {
$items = [];
if (LooseTypeCheck::isArray($schema->items)) {
$items = $schema->items;
} elseif (isset($schema->minItems) && count($value) < $schema->minItems) {
$items = array_fill(count($value), $schema->minItems - count($value), $schema->items);
}
// $value is an array, and items are defined - treat as plain array
foreach ($items as $currentItem => $itemDefinition) {
$itemDefinition = $this->factory->getSchemaStorage()->resolveRefSchema($itemDefinition);
if (
!array_key_exists($currentItem, $value)
&& property_exists($itemDefinition, 'default')
&& $this->shouldApplyDefaultValue($requiredOnly, $itemDefinition)) {
if (is_object($itemDefinition->default)) {
$value[$currentItem] = clone $itemDefinition->default;
} else {
$value[$currentItem] = $itemDefinition->default;
}
}
$path->setFromDefault();
}
} elseif (
$value instanceof self
&& property_exists($schema, 'default')
&& $this->shouldApplyDefaultValue($requiredOnly, $schema)) {
// $value is a leaf, not a container - apply the default directly
$value = is_object($schema->default) ? clone $schema->default : $schema->default;
$path->setFromDefault();
}
}
/**
* Validate allOf, anyOf, and oneOf properties
*
* @param mixed $value
* @param mixed $schema
* @param JsonPointer $path
* @param string $i
*/
protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i = '')
{
// Verify type
if ($value instanceof self) {
return;
}
if (isset($schema->allOf)) {
$isValid = true;
foreach ($schema->allOf as $allOf) {
$initErrors = $this->getErrors();
$this->checkUndefined($value, $allOf, $path, $i);
$isValid = $isValid && (count($this->getErrors()) == count($initErrors));
}
if (!$isValid) {
$this->addError(ConstraintError::ALL_OF(), $path);
}
}
if (isset($schema->anyOf)) {
$isValid = false;
$startErrors = $this->getErrors();
$coerceOrDefaults = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES | self::CHECK_MODE_APPLY_DEFAULTS);
foreach ($schema->anyOf as $anyOf) {
$initErrors = $this->getErrors();
try {
$anyOfValue = $coerceOrDefaults ? DeepCopy::copyOf($value) : $value;
$this->checkUndefined($anyOfValue, $anyOf, $path, $i);
if ($isValid = (count($this->getErrors()) === count($initErrors))) {
$value = $anyOfValue;
break;
}
} catch (ValidationException $e) {
$isValid = false;
}
}
if (!$isValid) {
$this->addError(ConstraintError::ANY_OF(), $path);
} else {
$this->errors = $startErrors;
}
}
if (isset($schema->oneOf)) {
$allErrors = [];
$matchedSchemas = [];
$startErrors = $this->getErrors();
$coerceOrDefaults = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES | self::CHECK_MODE_APPLY_DEFAULTS);
foreach ($schema->oneOf as $oneOf) {
try {
$this->errors = [];
$oneOfValue = $coerceOrDefaults ? DeepCopy::copyOf($value) : $value;
$this->checkUndefined($oneOfValue, $oneOf, $path, $i);
if (count($this->getErrors()) === 0) {
$matchedSchemas[] = ['schema' => $oneOf, 'value' => $oneOfValue];
}
$allErrors = array_merge($allErrors, array_values($this->getErrors()));
} catch (ValidationException $e) {
// deliberately do nothing here - validation failed, but we want to check
// other schema options in the OneOf field.
}
}
if (count($matchedSchemas) !== 1) {
$this->addErrors(array_merge($allErrors, $startErrors));
$this->addError(ConstraintError::ONE_OF(), $path);
} else {
$this->errors = $startErrors;
$value = $matchedSchemas[0]['value'];
}
}
}
/**
* Validate dependencies
*
* @param mixed $value
* @param mixed $dependencies
* @param JsonPointer $path
* @param string $i
*/
protected function validateDependencies($value, $dependencies, JsonPointer $path, $i = '')
{
foreach ($dependencies as $key => $dependency) {
if ($this->getTypeCheck()->propertyExists($value, $key)) {
if (is_string($dependency)) {
// Draft 3 string is allowed - e.g. "dependencies": {"bar": "foo"}
if (!$this->getTypeCheck()->propertyExists($value, $dependency)) {
$this->addError(ConstraintError::DEPENDENCIES(), $path, [
'key' => $key,
'dependency' => $dependency
]);
}
} elseif (is_array($dependency)) {
// Draft 4 must be an array - e.g. "dependencies": {"bar": ["foo"]}
foreach ($dependency as $d) {
if (!$this->getTypeCheck()->propertyExists($value, $d)) {
$this->addError(ConstraintError::DEPENDENCIES(), $path, [
'key' => $key,
'dependency' => $dependency
]);
}
}
} elseif (is_object($dependency)) {
// Schema - e.g. "dependencies": {"bar": {"properties": {"foo": {...}}}}
$this->checkUndefined($value, $dependency, $path, $i);
}
}
}
}
protected function validateUri($schema, $schemaUri = null)
{
$resolver = new UriResolver();
$retriever = $this->factory->getUriRetriever();
$jsonSchema = null;
if ($resolver->isValid($schemaUri)) {
$schemaId = property_exists($schema, 'id') ? $schema->id : null;
$jsonSchema = $retriever->retrieve($schemaId, $schemaUri);
}
return $jsonSchema;
}
}
@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Entity;
use JsonSchema\Exception\InvalidArgumentException;
/**
* @package JsonSchema\Entity
*
* @author Joost Nijhuis <jnijhuis81@gmail.com>
*/
class JsonPointer
{
/** @var string */
private $filename;
/** @var string[] */
private $propertyPaths = [];
/**
* @var bool Whether the value at this path was set from a schema default
*/
private $fromDefault = false;
/**
* @param string $value
*
* @throws InvalidArgumentException when $value is not a string
*/
public function __construct($value)
{
if (!is_string($value)) {
throw new InvalidArgumentException('Ref value must be a string');
}
$splitRef = explode('#', $value, 2);
$this->filename = $splitRef[0];
if (array_key_exists(1, $splitRef)) {
$this->propertyPaths = $this->decodePropertyPaths($splitRef[1]);
}
}
/**
* @param string $propertyPathString
*
* @return string[]
*/
private function decodePropertyPaths($propertyPathString)
{
$paths = [];
foreach (explode('/', trim($propertyPathString, '/')) as $path) {
$path = $this->decodePath($path);
if (is_string($path) && '' !== $path) {
$paths[] = $path;
}
}
return $paths;
}
/**
* @return array
*/
private function encodePropertyPaths()
{
return array_map(
[$this, 'encodePath'],
$this->getPropertyPaths()
);
}
/**
* @param string $path
*
* @return string
*/
private function decodePath($path)
{
return strtr($path, ['~1' => '/', '~0' => '~', '%25' => '%']);
}
/**
* @param string $path
*
* @return string
*/
private function encodePath($path)
{
return strtr($path, ['/' => '~1', '~' => '~0', '%' => '%25']);
}
/**
* @return string
*/
public function getFilename()
{
return $this->filename;
}
/**
* @return string[]
*/
public function getPropertyPaths()
{
return $this->propertyPaths;
}
/**
* @param array $propertyPaths
*
* @return JsonPointer
*/
public function withPropertyPaths(array $propertyPaths)
{
$new = clone $this;
$new->propertyPaths = array_map(function ($p): string { return (string) $p; }, $propertyPaths);
return $new;
}
/**
* @return string
*/
public function getPropertyPathAsString()
{
return rtrim('#/' . implode('/', $this->encodePropertyPaths()), '/');
}
/**
* @return string
*/
public function __toString()
{
return $this->getFilename() . $this->getPropertyPathAsString();
}
/**
* Mark the value at this path as being set from a schema default
*/
public function setFromDefault(): void
{
$this->fromDefault = true;
}
/**
* Check whether the value at this path was set from a schema default
*
* @return bool
*/
public function fromDefault()
{
return $this->fromDefault;
}
}
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace JsonSchema;
abstract class Enum extends \MabeEnum\Enum
{
}
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Exception;
interface ExceptionInterface
{
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Exception;
/**
* Wrapper for the InvalidArgumentException
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Exception;
/**
* Wrapper for the ResourceNotFoundException
*/
class InvalidConfigException extends RuntimeException
{
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Exception;
/**
* Wrapper for the InvalidSchemaMediaType
*/
class InvalidSchemaException extends RuntimeException
{
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Exception;
/**
* Wrapper for the InvalidSchemaMediaType
*/
class InvalidSchemaMediaTypeException extends RuntimeException
{
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Exception;
/**
* Wrapper for the InvalidSourceUriException
*/
class InvalidSourceUriException extends InvalidArgumentException
{
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Exception;
/**
* Wrapper for the JsonDecodingException
*/
class JsonDecodingException extends RuntimeException
{
public function __construct($code = JSON_ERROR_NONE, ?\Exception $previous = null)
{
switch ($code) {
case JSON_ERROR_DEPTH:
$message = 'The maximum stack depth has been exceeded';
break;
case JSON_ERROR_STATE_MISMATCH:
$message = 'Invalid or malformed JSON';
break;
case JSON_ERROR_CTRL_CHAR:
$message = 'Control character error, possibly incorrectly encoded';
break;
case JSON_ERROR_UTF8:
$message = 'Malformed UTF-8 characters, possibly incorrectly encoded';
break;
case JSON_ERROR_SYNTAX:
$message = 'JSON syntax is malformed';
break;
default:
$message = 'Syntax error';
}
parent::__construct($message, $code, $previous);
}
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Exception;
/**
* Wrapper for the ResourceNotFoundException
*/
class ResourceNotFoundException extends RuntimeException
{
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Exception;
/**
* Wrapper for the RuntimeException
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Exception;
/**
* @package JsonSchema\Exception
*
* @author Joost Nijhuis <jnijhuis81@gmail.com>
*/
class UnresolvableJsonPointerException extends InvalidArgumentException
{
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Exception;
/**
* Wrapper for the UriResolverException
*/
class UriResolverException extends RuntimeException
{
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Exception;
class ValidationException extends RuntimeException
{
}
@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Iterator;
/**
* @package JsonSchema\Iterator
*
* @author Joost Nijhuis <jnijhuis81@gmail.com>
*/
class ObjectIterator implements \Iterator, \Countable
{
/** @var object */
private $object;
/** @var int */
private $position = 0;
/** @var array */
private $data = [];
/** @var bool */
private $initialized = false;
/**
* @param object $object
*/
public function __construct($object)
{
$this->object = $object;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function current()
{
$this->initialize();
return $this->data[$this->position];
}
/**
* {@inheritdoc}
*/
public function next(): void
{
$this->initialize();
$this->position++;
}
/**
* {@inheritdoc}
*/
public function key(): int
{
$this->initialize();
return $this->position;
}
/**
* {@inheritdoc}
*/
public function valid(): bool
{
$this->initialize();
return isset($this->data[$this->position]);
}
/**
* {@inheritdoc}
*/
public function rewind(): void
{
$this->initialize();
$this->position = 0;
}
/**
* {@inheritdoc}
*/
public function count(): int
{
$this->initialize();
return count($this->data);
}
/**
* Initializer
*/
private function initialize()
{
if (!$this->initialized) {
$this->data = $this->buildDataFromObject($this->object);
$this->initialized = true;
}
}
/**
* @param object $object
*
* @return array
*/
private function buildDataFromObject($object)
{
$result = [];
$stack = new \SplStack();
$stack->push($object);
while (!$stack->isEmpty()) {
$current = $stack->pop();
if (is_object($current)) {
array_push($result, $current);
}
foreach ($this->getDataFromItem($current) as $propertyName => $propertyValue) {
if (is_object($propertyValue) || is_array($propertyValue)) {
$stack->push($propertyValue);
}
}
}
return $result;
}
/**
* @param object|array $item
*
* @return array
*/
private function getDataFromItem($item)
{
if (!is_object($item) && !is_array($item)) {
return [];
}
return is_object($item) ? get_object_vars($item) : $item;
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace JsonSchema;
class Rfc3339
{
private const REGEX = '/^(\d{4}-\d{2}-\d{2}[T ]{1}\d{2}:\d{2}:\d{2})(\.\d+)?(Z|([+-]\d{2}):?(\d{2}))$/';
/**
* Try creating a DateTime instance
*
* @param string $string
*
* @return \DateTime|null
*/
public static function createFromString($string)
{
if (!preg_match(self::REGEX, strtoupper($string), $matches)) {
return null;
}
$dateAndTime = $matches[1];
$microseconds = $matches[2] ?: '.000000';
$timeZone = 'Z' !== $matches[3] ? $matches[4] . ':' . $matches[5] : '+00:00';
$dateFormat = strpos($dateAndTime, 'T') === false ? 'Y-m-d H:i:s.uP' : 'Y-m-d\TH:i:s.uP';
$dateTime = \DateTime::createFromFormat($dateFormat, $dateAndTime . $microseconds . $timeZone, new \DateTimeZone('UTC'));
return $dateTime ?: null;
}
}
@@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace JsonSchema;
use JsonSchema\Constraints\BaseConstraint;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Exception\UnresolvableJsonPointerException;
use JsonSchema\Uri\UriResolver;
use JsonSchema\Uri\UriRetriever;
class SchemaStorage implements SchemaStorageInterface
{
public const INTERNAL_PROVIDED_SCHEMA_URI = 'internal://provided-schema/';
protected $uriRetriever;
protected $uriResolver;
protected $schemas = [];
public function __construct(
?UriRetrieverInterface $uriRetriever = null,
?UriResolverInterface $uriResolver = null
) {
$this->uriRetriever = $uriRetriever ?: new UriRetriever();
$this->uriResolver = $uriResolver ?: new UriResolver();
}
/**
* @return UriRetrieverInterface
*/
public function getUriRetriever()
{
return $this->uriRetriever;
}
/**
* @return UriResolverInterface
*/
public function getUriResolver()
{
return $this->uriResolver;
}
/**
* {@inheritdoc}
*/
public function addSchema(string $id, $schema = null): void
{
if (is_null($schema) && $id !== self::INTERNAL_PROVIDED_SCHEMA_URI) {
// if the schema was user-provided to Validator and is still null, then assume this is
// what the user intended, as there's no way for us to retrieve anything else. User-supplied
// schemas do not have an associated URI when passed via Validator::validate().
$schema = $this->uriRetriever->retrieve($id);
}
// cast array schemas to object
if (is_array($schema)) {
$schema = BaseConstraint::arrayToObjectRecursive($schema);
}
// workaround for bug in draft-03 & draft-04 meta-schemas (id & $ref defined with incorrect format)
// see https://github.com/json-schema-org/JSON-Schema-Test-Suite/issues/177#issuecomment-293051367
if (is_object($schema) && property_exists($schema, 'id')) {
if ($schema->id === 'http://json-schema.org/draft-04/schema#') {
$schema->properties->id->format = 'uri-reference';
} elseif ($schema->id === 'http://json-schema.org/draft-03/schema#') {
$schema->properties->id->format = 'uri-reference';
$schema->properties->{'$ref'}->format = 'uri-reference';
}
}
$this->scanForSubschemas($schema, $id);
// resolve references
$this->expandRefs($schema, $id);
$this->schemas[$id] = $schema;
}
/**
* Recursively resolve all references against the provided base
*
* @param mixed $schema
*/
private function expandRefs(&$schema, ?string $parentId = null): void
{
if (!is_object($schema)) {
if (is_array($schema)) {
foreach ($schema as &$member) {
$this->expandRefs($member, $parentId);
}
}
return;
}
if (property_exists($schema, '$ref') && is_string($schema->{'$ref'})) {
$refPointer = new JsonPointer($this->uriResolver->resolve($schema->{'$ref'}, $parentId));
$schema->{'$ref'} = (string) $refPointer;
}
foreach ($schema as $propertyName => &$member) {
if (in_array($propertyName, ['enum', 'const'])) {
// Enum and const don't allow $ref as a keyword, see https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/445
continue;
}
$childId = $parentId;
if (property_exists($schema, 'id') && is_string($schema->id) && $childId !== $schema->id) {
$childId = $this->uriResolver->resolve($schema->id, $childId);
}
$this->expandRefs($member, $childId);
}
}
/**
* {@inheritdoc}
*/
public function getSchema(string $id)
{
if (!array_key_exists($id, $this->schemas)) {
$this->addSchema($id);
}
return $this->schemas[$id];
}
/**
* {@inheritdoc}
*/
public function resolveRef(string $ref, $resolveStack = [])
{
$jsonPointer = new JsonPointer($ref);
// resolve filename for pointer
$fileName = $jsonPointer->getFilename();
if (!strlen($fileName)) {
throw new UnresolvableJsonPointerException(sprintf(
"Could not resolve fragment '%s': no file is defined",
$jsonPointer->getPropertyPathAsString()
));
}
// get & process the schema
$refSchema = $this->getSchema($fileName);
foreach ($jsonPointer->getPropertyPaths() as $path) {
if (is_object($refSchema) && property_exists($refSchema, $path)) {
$refSchema = $this->resolveRefSchema($refSchema->{$path}, $resolveStack);
} elseif (is_array($refSchema) && array_key_exists($path, $refSchema)) {
$refSchema = $this->resolveRefSchema($refSchema[$path], $resolveStack);
} else {
throw new UnresolvableJsonPointerException(sprintf(
'File: %s is found, but could not resolve fragment: %s',
$jsonPointer->getFilename(),
$jsonPointer->getPropertyPathAsString()
));
}
}
return $refSchema;
}
/**
* {@inheritdoc}
*/
public function resolveRefSchema($refSchema, $resolveStack = [])
{
if (is_object($refSchema) && property_exists($refSchema, '$ref') && is_string($refSchema->{'$ref'})) {
if (in_array($refSchema, $resolveStack, true)) {
throw new UnresolvableJsonPointerException(sprintf(
'Dereferencing a pointer to %s results in an infinite loop',
$refSchema->{'$ref'}
));
}
$resolveStack[] = $refSchema;
return $this->resolveRef($refSchema->{'$ref'}, $resolveStack);
}
return $refSchema;
}
/**
* @param mixed $schema
*/
private function scanForSubschemas($schema, string $parentId): void
{
if (!$schema instanceof \stdClass && !is_array($schema)) {
return;
}
foreach ($schema as $propertyName => $potentialSubSchema) {
if (!is_object($potentialSubSchema)) {
continue;
}
if (property_exists($potentialSubSchema, 'id') && is_string($potentialSubSchema->id) && property_exists($potentialSubSchema, 'type')) {
// Enum and const don't allow id as a keyword, see https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/471
if (in_array($propertyName, ['enum', 'const'])) {
continue;
}
// Found sub schema
$this->addSchema($this->uriResolver->resolve($potentialSubSchema->id, $parentId), $potentialSubSchema);
}
$this->scanForSubschemas($potentialSubSchema, $parentId);
}
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace JsonSchema;
interface SchemaStorageInterface
{
/**
* Adds schema with given identifier
*
* @param object $schema
*/
public function addSchema(string $id, $schema = null): void;
/**
* Returns schema for given identifier, or null if it does not exist
*
* @return object
*/
public function getSchema(string $id);
/**
* Returns schema for given reference with all sub-references resolved
*
* @return object
*/
public function resolveRef(string $ref);
/**
* Returns schema referenced by '$ref' property
*
* @param mixed $refSchema
*
* @return object
*/
public function resolveRefSchema($refSchema);
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Tool;
class DeepComparer
{
/**
* @param mixed $left
* @param mixed $right
*/
public static function isEqual($left, $right): bool
{
if ($left === null && $right === null) {
return true;
}
$isLeftScalar = is_scalar($left);
$isLeftNumber = is_int($left) || is_float($left);
$isRightScalar = is_scalar($right);
$isRightNumber = is_int($right) || is_float($right);
if ($isLeftScalar && $isRightScalar) {
/*
* In Json-Schema mathematically equal numbers are compared equal
*/
if ($isLeftNumber && $isRightNumber && (float) $left === (float) $right) {
return true;
}
return $left === $right;
}
if ($isLeftScalar !== $isRightScalar) {
return false;
}
if (is_array($left) && is_array($right)) {
return self::isArrayEqual($left, $right);
}
if ($left instanceof \stdClass && $right instanceof \stdClass) {
return self::isArrayEqual((array) $left, (array) $right);
}
return false;
}
/**
* @param array<string|int, mixed> $left
* @param array<string|int, mixed> $right
*/
private static function isArrayEqual(array $left, array $right): bool
{
if (count($left) !== count($right)) {
return false;
}
foreach ($left as $key => $value) {
if (!array_key_exists($key, $right)) {
return false;
}
if (!self::isEqual($value, $right[$key])) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Tool;
use JsonSchema\Exception\JsonDecodingException;
use JsonSchema\Exception\RuntimeException;
class DeepCopy
{
/**
* @param mixed $input
*
* @return mixed
*/
public static function copyOf($input)
{
$json = json_encode($input);
if (JSON_ERROR_NONE < $error = json_last_error()) {
throw new JsonDecodingException($error);
}
if ($json === false) {
throw new RuntimeException('Failed to encode input to JSON: ' . json_last_error_msg());
}
return json_decode($json, self::isAssociativeArray($input));
}
/**
* @param mixed $input
*/
private static function isAssociativeArray($input): bool
{
return is_array($input) && array_keys($input) !== range(0, count($input) - 1);
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Tool\Validator;
class RelativeReferenceValidator
{
public static function isValid(string $ref): bool
{
// Relative reference pattern as per RFC 3986, Section 4.1
$pattern = '/^(([^\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/';
if (preg_match($pattern, $ref) !== 1) {
return false;
}
// Additional checks for invalid cases
if (preg_match('/^(http|https):\/\//', $ref)) {
return false; // Absolute URI
}
if (preg_match('/^:\/\//', $ref)) {
return false; // Missing scheme in authority
}
if (preg_match('/^:\//', $ref)) {
return false; // Invalid scheme separator
}
if (preg_match('/^\/\/$/', $ref)) {
return false; // Empty authority
}
if (preg_match('/^\/\/\/[^\/]/', $ref)) {
return false; // Invalid authority with three slashes
}
if (preg_match('/\s/', $ref)) {
return false; // Spaces are not allowed in URIs
}
if (preg_match('/^\?#|^#$/', $ref)) {
return false; // Missing path but having query and fragment
}
if ($ref === '#' || $ref === '?') {
return false; // Missing path and having only fragment or query
}
return true;
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Tool\Validator;
class UriValidator
{
public static function isValid(string $uri): bool
{
// RFC 3986: Hierarchical URIs (http, https, ftp, etc.)
$hierarchicalPattern = '/^
([a-z][a-z0-9+\-.]*):\/\/ # Scheme (http, https, ftp, etc.)
(?:([^:@\/?#]+)(?::([^@\/?#]*))?@)? # Optional userinfo (user:pass@)
([a-z0-9.-]+|\[[a-f0-9:.]+\]) # Hostname or IPv6 in brackets
(?::(\d{1,5}))? # Optional port
(\/[a-zA-Z0-9._~!$&\'()*+,;=:@\/%-]*)* # Path (valid characters only)
(\?([^#]*))? # Optional query
(\#(.*))? # Optional fragment
$/ix';
// RFC 3986: Non-Hierarchical URIs (mailto, data, urn)
$nonHierarchicalPattern = '/^
(mailto|data|urn): # Only allow known non-hierarchical schemes
(.+) # Must contain at least one character after scheme
$/ix';
// RFC 5322-compliant email validation for `mailto:` URIs
$emailPattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
// First, check if it's a valid hierarchical URI
if (preg_match($hierarchicalPattern, $uri, $matches) === 1) {
// Validate domain name (no double dots like example..com)
if (!empty($matches[4]) && preg_match('/\.\./', $matches[4])) {
return false;
}
// Validate port (should be between 1 and 65535 if specified)
if (!empty($matches[5]) && ($matches[5] < 1 || $matches[5] > 65535)) {
return false;
}
// Validate path (reject illegal characters: < > { } | \ ^ `)
if (!empty($matches[6]) && preg_match('/[<>{}|\\\^`]/', $matches[6])) {
return false;
}
return true;
}
// If not hierarchical, check non-hierarchical URIs
if (preg_match($nonHierarchicalPattern, $uri, $matches) === 1) {
$scheme = strtolower($matches[1]); // Extract the scheme
// Special case: `mailto:` must contain a **valid email address**
if ($scheme === 'mailto') {
return preg_match($emailPattern, $matches[2]) === 1;
}
return true; // Valid non-hierarchical URI
}
return false;
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* JsonSchema
*
* @filesource
*/
namespace JsonSchema\Uri\Retrievers;
/**
* AbstractRetriever implements the default shared behavior
* that all descendant Retrievers should inherit
*
* @author Steven Garcia <webwhammy@gmail.com>
*/
abstract class AbstractRetriever implements UriRetrieverInterface
{
/**
* Media content type
*
* @var string
*/
protected $contentType;
/**
* {@inheritdoc}
*
* @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::getContentType()
*/
public function getContentType()
{
return $this->contentType;
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Uri\Retrievers;
use JsonSchema\Exception\RuntimeException;
use JsonSchema\Validator;
/**
* Tries to retrieve JSON schemas from a URI using cURL library
*
* @author Sander Coolen <sander@jibber.nl>
*/
class Curl extends AbstractRetriever
{
protected $messageBody;
public function __construct()
{
if (!function_exists('curl_init')) {
// Cannot test this, because curl_init is present on all test platforms plus mock
throw new RuntimeException('cURL not installed'); // @codeCoverageIgnore
}
}
/**
* {@inheritdoc}
*
* @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve()
*/
public function retrieve($uri)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $uri);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: ' . Validator::SCHEMA_MEDIA_TYPE]);
$response = curl_exec($ch);
if (false === $response) {
throw new \JsonSchema\Exception\ResourceNotFoundException('JSON schema not found');
}
$this->fetchMessageBody($response);
$this->fetchContentType($response);
curl_close($ch);
return $this->messageBody;
}
/**
* @param string $response cURL HTTP response
*/
private function fetchMessageBody($response)
{
preg_match("/(?:\r\n){2}(.*)$/ms", $response, $match);
$this->messageBody = $match[1];
}
/**
* @param string $response cURL HTTP response
*
* @return bool Whether the Content-Type header was found or not
*/
protected function fetchContentType($response)
{
if (0 < preg_match("/Content-Type:(\V*)/ims", $response, $match)) {
$this->contentType = trim($match[1]);
return true;
}
return false;
}
}
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Uri\Retrievers;
use JsonSchema\Exception\ResourceNotFoundException;
/**
* Tries to retrieve JSON schemas from a URI using file_get_contents()
*
* @author Sander Coolen <sander@jibber.nl>
*/
class FileGetContents extends AbstractRetriever
{
protected $messageBody;
/**
* {@inheritdoc}
*
* @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve()
*/
public function retrieve($uri)
{
$errorMessage = null;
set_error_handler(function ($errno, $errstr) use (&$errorMessage) {
$errorMessage = $errstr;
});
$response = file_get_contents($uri);
restore_error_handler();
if ($errorMessage) {
throw new ResourceNotFoundException($errorMessage);
}
if (false === $response) {
throw new ResourceNotFoundException('JSON schema not found at ' . $uri);
}
if ($response == ''
&& substr($uri, 0, 7) == 'file://' && substr($uri, -1) == '/'
) {
throw new ResourceNotFoundException('JSON schema not found at ' . $uri);
}
$this->messageBody = $response;
if (!empty($http_response_header)) {
// $http_response_header cannot be tested, because it's defined in the method's local scope
// See http://php.net/manual/en/reserved.variables.httpresponseheader.php for more info.
$this->fetchContentType($http_response_header); // @codeCoverageIgnore
} else { // @codeCoverageIgnore
// Could be a "file://" url or something else - fake up the response
$this->contentType = null;
}
return $this->messageBody;
}
/**
* @param array $headers HTTP Response Headers
*
* @return bool Whether the Content-Type header was found or not
*/
private function fetchContentType(array $headers)
{
foreach (array_reverse($headers) as $header) {
if ($this->contentType = self::getContentTypeMatchInHeader($header)) {
return true;
}
}
return false;
}
/**
* @param string $header
*
* @return string|null
*/
protected static function getContentTypeMatchInHeader($header)
{
if (0 < preg_match("/Content-Type:(\V*)/ims", $header, $match)) {
return trim($match[1]);
}
return null;
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace JsonSchema\Uri\Retrievers;
use JsonSchema\Validator;
/**
* URI retrieved based on a predefined array of schemas
*
* @example
*
* $retriever = new PredefinedArray(array(
* 'http://acme.com/schemas/person#' => '{ ... }',
* 'http://acme.com/schemas/address#' => '{ ... }',
* ))
*
* $schema = $retriever->retrieve('http://acme.com/schemas/person#');
*/
class PredefinedArray extends AbstractRetriever
{
/**
* Contains schemas as URI => JSON
*
* @var array
*/
private $schemas;
/**
* Constructor
*
* @param array $schemas
* @param string $contentType
*/
public function __construct(array $schemas, $contentType = Validator::SCHEMA_MEDIA_TYPE)
{
$this->schemas = $schemas;
$this->contentType = $contentType;
}
/**
* {@inheritdoc}
*
* @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve()
*/
public function retrieve($uri)
{
if (!array_key_exists($uri, $this->schemas)) {
throw new \JsonSchema\Exception\ResourceNotFoundException(sprintf(
'The JSON schema "%s" was not found.',
$uri
));
}
return $this->schemas[$uri];
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Uri\Retrievers;
/**
* Interface for URI retrievers
*
* @author Sander Coolen <sander@jibber.nl>
*/
interface UriRetrieverInterface
{
/**
* Retrieve a schema from the specified URI
*
* @param string $uri URI that resolves to a JSON schema
*
* @throws \JsonSchema\Exception\ResourceNotFoundException
*
* @return mixed string|null
*/
public function retrieve($uri);
/**
* Get media content type
*
* @return string
*/
public function getContentType();
}
@@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Uri;
use JsonSchema\Exception\UriResolverException;
use JsonSchema\UriResolverInterface;
/**
* Resolves JSON Schema URIs
*
* @author Sander Coolen <sander@jibber.nl>
*/
class UriResolver implements UriResolverInterface
{
/**
* Parses a URI into five main components
*
* @param string $uri
*
* @return array
*/
public function parse($uri)
{
preg_match('|^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?|', (string) $uri, $match);
$components = [];
if (5 < count($match)) {
$components = [
'scheme' => $match[2],
'authority' => $match[4],
'path' => $match[5]
];
}
if (7 < count($match)) {
$components['query'] = $match[7];
}
if (9 < count($match)) {
$components['fragment'] = $match[9];
}
return $components;
}
/**
* Builds a URI based on n array with the main components
*
* @param array $components
*
* @return string
*/
public function generate(array $components)
{
$uri = $components['scheme'] . '://'
. $components['authority']
. $components['path'];
if (array_key_exists('query', $components) && strlen($components['query'])) {
$uri .= '?' . $components['query'];
}
if (array_key_exists('fragment', $components)) {
$uri .= '#' . $components['fragment'];
}
return $uri;
}
/**
* {@inheritdoc}
*/
public function resolve($uri, $baseUri = null)
{
// treat non-uri base as local file path
if (
!is_null($baseUri) &&
!filter_var($baseUri, \FILTER_VALIDATE_URL) &&
!preg_match('|^[^/]+://|u', $baseUri)
) {
if (is_file($baseUri)) {
$baseUri = 'file://' . realpath($baseUri);
} elseif (is_dir($baseUri)) {
$baseUri = 'file://' . realpath($baseUri) . '/';
} else {
$baseUri = 'file://' . getcwd() . '/' . $baseUri;
}
}
if ($uri == '') {
return $baseUri;
}
$components = $this->parse($uri);
$path = $components['path'];
if (!empty($components['scheme'])) {
return $uri;
}
$baseComponents = $this->parse($baseUri);
$basePath = $baseComponents['path'];
$baseComponents['path'] = self::combineRelativePathWithBasePath($path, $basePath);
if (isset($components['fragment'])) {
$baseComponents['fragment'] = $components['fragment'];
}
return $this->generate($baseComponents);
}
/**
* Tries to glue a relative path onto an absolute one
*
* @param string $relativePath
* @param string $basePath
*
* @throws UriResolverException
*
* @return string Merged path
*/
public static function combineRelativePathWithBasePath($relativePath, $basePath)
{
$relativePath = self::normalizePath($relativePath);
if (!$relativePath) {
return $basePath;
}
if ($relativePath[0] === '/') {
return $relativePath;
}
if (!$basePath) {
throw new UriResolverException(sprintf("Unable to resolve URI '%s' from base '%s'", $relativePath, $basePath));
}
$dirname = $basePath[strlen($basePath) - 1] === '/' ? $basePath : dirname($basePath);
$combined = rtrim($dirname, '/') . '/' . ltrim($relativePath, '/');
$combinedSegments = explode('/', $combined);
$collapsedSegments = [];
while ($combinedSegments) {
$segment = array_shift($combinedSegments);
if ($segment === '..') {
if (count($collapsedSegments) <= 1) {
// Do not remove the top level (domain)
// This is not ideal - the domain should not be part of the path here. parse() and generate()
// should handle the "domain" separately, like the schema.
// Then the if-condition here would be `if (!$collapsedSegments) {`.
throw new UriResolverException(sprintf("Unable to resolve URI '%s' from base '%s'", $relativePath, $basePath));
}
array_pop($collapsedSegments);
} else {
$collapsedSegments[] = $segment;
}
}
return implode('/', $collapsedSegments);
}
/**
* Normalizes a URI path component by removing dot-slash and double slashes
*
* @param string $path
*
* @return string
*/
private static function normalizePath($path)
{
$path = preg_replace('|((?<!\.)\./)*|', '', $path);
$path = preg_replace('|//|', '/', $path);
return $path;
}
/**
* @param string $uri
*
* @return bool
*/
public function isValid($uri)
{
$components = $this->parse($uri);
return !empty($components);
}
}
@@ -0,0 +1,351 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema\Uri;
use JsonSchema\Exception\InvalidSchemaMediaTypeException;
use JsonSchema\Exception\JsonDecodingException;
use JsonSchema\Exception\ResourceNotFoundException;
use JsonSchema\Uri\Retrievers\FileGetContents;
use JsonSchema\Uri\Retrievers\UriRetrieverInterface;
use JsonSchema\UriRetrieverInterface as BaseUriRetrieverInterface;
use JsonSchema\Validator;
/**
* Retrieves JSON Schema URIs
*
* @author Tyler Akins <fidian@rumkin.com>
*/
class UriRetriever implements BaseUriRetrieverInterface
{
/**
* @var array Map of URL translations
*/
protected $translationMap = [
// use local copies of the spec schemas
'|^https?://json-schema.org/draft-(0[34])/schema#?|' => 'package://dist/schema/json-schema-draft-$1.json'
];
/**
* @var array A list of endpoints for media type check exclusion
*/
protected $allowedInvalidContentTypeEndpoints = [
'http://json-schema.org/',
'https://json-schema.org/'
];
/**
* @var null|UriRetrieverInterface
*/
protected $uriRetriever = null;
/**
* @var array|object[]
*
* @see loadSchema
*/
private $schemaCache = [];
/**
* Adds an endpoint to the media type validation exclusion list
*
* @param string $endpoint
*/
public function addInvalidContentTypeEndpoint($endpoint)
{
$this->allowedInvalidContentTypeEndpoints[] = $endpoint;
}
/**
* Guarantee the correct media type was encountered
*
* @param UriRetrieverInterface $uriRetriever
* @param string $uri
*
* @return bool|void
*/
public function confirmMediaType($uriRetriever, $uri)
{
$contentType = $uriRetriever->getContentType();
if (is_null($contentType)) {
// Well, we didn't get an invalid one
return;
}
if (in_array($contentType, [Validator::SCHEMA_MEDIA_TYPE, 'application/json'])) {
return;
}
foreach ($this->allowedInvalidContentTypeEndpoints as $endpoint) {
if (!\is_null($uri) && strpos($uri, $endpoint) === 0) {
return true;
}
}
throw new InvalidSchemaMediaTypeException(sprintf('Media type %s expected', Validator::SCHEMA_MEDIA_TYPE));
}
/**
* Get a URI Retriever
*
* If none is specified, sets a default FileGetContents retriever and
* returns that object.
*
* @return UriRetrieverInterface
*/
public function getUriRetriever()
{
if (is_null($this->uriRetriever)) {
$this->setUriRetriever(new FileGetContents());
}
return $this->uriRetriever;
}
/**
* Resolve a schema based on pointer
*
* URIs can have a fragment at the end in the format of
* #/path/to/object and we are to look up the 'path' property of
* the first object then the 'to' and 'object' properties.
*
* @param object $jsonSchema JSON Schema contents
* @param string $uri JSON Schema URI
*
* @throws ResourceNotFoundException
*
* @return object JSON Schema after walking down the fragment pieces
*/
public function resolvePointer($jsonSchema, $uri)
{
$resolver = new UriResolver();
$parsed = $resolver->parse($uri);
if (empty($parsed['fragment'])) {
return $jsonSchema;
}
$path = explode('/', $parsed['fragment']);
while ($path) {
$pathElement = array_shift($path);
if (!empty($pathElement)) {
$pathElement = str_replace('~1', '/', $pathElement);
$pathElement = str_replace('~0', '~', $pathElement);
if (!empty($jsonSchema->$pathElement)) {
$jsonSchema = $jsonSchema->$pathElement;
} else {
throw new ResourceNotFoundException(
'Fragment "' . $parsed['fragment'] . '" not found'
. ' in ' . $uri
);
}
if (!is_object($jsonSchema)) {
throw new ResourceNotFoundException(
'Fragment part "' . $pathElement . '" is no object '
. ' in ' . $uri
);
}
}
}
return $jsonSchema;
}
/**
* {@inheritdoc}
*/
public function retrieve($uri, $baseUri = null, $translate = true)
{
$resolver = new UriResolver();
$resolvedUri = $fetchUri = $resolver->resolve($uri, $baseUri);
//fetch URL without #fragment
$arParts = $resolver->parse($resolvedUri);
if (isset($arParts['fragment'])) {
unset($arParts['fragment']);
$fetchUri = $resolver->generate($arParts);
}
// apply URI translations
if ($translate) {
$fetchUri = $this->translate($fetchUri);
}
$jsonSchema = $this->loadSchema($fetchUri);
// Use the JSON pointer if specified
$jsonSchema = $this->resolvePointer($jsonSchema, $resolvedUri);
if ($jsonSchema instanceof \stdClass) {
$jsonSchema->id = $resolvedUri;
}
return $jsonSchema;
}
/**
* Fetch a schema from the given URI, json-decode it and return it.
* Caches schema objects.
*
* @param string $fetchUri Absolute URI
*
* @return object JSON schema object
*/
protected function loadSchema($fetchUri)
{
if (isset($this->schemaCache[$fetchUri])) {
return $this->schemaCache[$fetchUri];
}
$uriRetriever = $this->getUriRetriever();
$contents = $this->uriRetriever->retrieve($fetchUri);
$this->confirmMediaType($uriRetriever, $fetchUri);
$jsonSchema = json_decode($contents);
if (JSON_ERROR_NONE < $error = json_last_error()) {
throw new JsonDecodingException($error);
}
$this->schemaCache[$fetchUri] = $jsonSchema;
return $jsonSchema;
}
/**
* Set the URI Retriever
*
* @param UriRetrieverInterface $uriRetriever
*
* @return $this for chaining
*/
public function setUriRetriever(UriRetrieverInterface $uriRetriever)
{
$this->uriRetriever = $uriRetriever;
return $this;
}
/**
* Parses a URI into five main components
*
* @param string $uri
*
* @return array
*/
public function parse($uri)
{
preg_match('|^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?|', $uri, $match);
$components = [];
if (5 < count($match)) {
$components = [
'scheme' => $match[2],
'authority' => $match[4],
'path' => $match[5]
];
}
if (7 < count($match)) {
$components['query'] = $match[7];
}
if (9 < count($match)) {
$components['fragment'] = $match[9];
}
return $components;
}
/**
* Builds a URI based on n array with the main components
*
* @param array $components
*
* @return string
*/
public function generate(array $components)
{
$uri = $components['scheme'] . '://'
. $components['authority']
. $components['path'];
if (array_key_exists('query', $components)) {
$uri .= $components['query'];
}
if (array_key_exists('fragment', $components)) {
$uri .= $components['fragment'];
}
return $uri;
}
/**
* Resolves a URI
*
* @param string $uri Absolute or relative
* @param string $baseUri Optional base URI
*
* @return string
*/
public function resolve($uri, $baseUri = null)
{
$components = $this->parse($uri);
$path = $components['path'];
if ((array_key_exists('scheme', $components)) && ('http' === $components['scheme'])) {
return $uri;
}
$baseComponents = $this->parse($baseUri);
$basePath = $baseComponents['path'];
$baseComponents['path'] = UriResolver::combineRelativePathWithBasePath($path, $basePath);
return $this->generate($baseComponents);
}
/**
* @param string $uri
*
* @return bool
*/
public function isValid($uri)
{
$components = $this->parse($uri);
return !empty($components);
}
/**
* Set a URL translation rule
*/
public function setTranslation($from, $to)
{
$this->translationMap[$from] = $to;
}
/**
* Apply URI translation rules
*/
public function translate($uri)
{
foreach ($this->translationMap as $from => $to) {
$uri = preg_replace($from, $to, $uri);
}
// translate references to local files within the json-schema package
$uri = preg_replace('|^package://|', sprintf('file://%s/', realpath(__DIR__ . '/../../..')), $uri);
return $uri;
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema;
/**
* @package JsonSchema
*/
interface UriResolverInterface
{
/**
* Resolves a URI
*
* @param string $uri Absolute or relative
* @param null|string $baseUri Optional base URI
*
* @return string Absolute URI
*/
public function resolve($uri, $baseUri = null);
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema;
/**
* @package JsonSchema
*/
interface UriRetrieverInterface
{
/**
* Retrieve a URI
*
* @param string $uri JSON Schema URI
* @param null|string $baseUri
*
* @return object JSON Schema contents
*/
public function retrieve($uri, $baseUri = null);
}
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace JsonSchema;
use JsonSchema\Constraints\BaseConstraint;
use JsonSchema\Constraints\Constraint;
use JsonSchema\Constraints\TypeCheck\LooseTypeCheck;
/**
* A JsonSchema Constraint
*
* @author Robert Schönthal <seroscho@googlemail.com>
* @author Bruno Prieto Reis <bruno.p.reis@gmail.com>
*
* @see README.md
*/
class Validator extends BaseConstraint
{
public const SCHEMA_MEDIA_TYPE = 'application/schema+json';
public const ERROR_NONE = 0;
public const ERROR_ALL = -1;
public const ERROR_DOCUMENT_VALIDATION = 1;
public const ERROR_SCHEMA_VALIDATION = 2;
/**
* Validates the given data against the schema and returns an object containing the results
* Both the php object and the schema are supposed to be a result of a json_decode call.
* The validation works as defined by the schema proposal in http://json-schema.org.
*
* Note that the first argument is passed by reference, so you must pass in a variable.
*
* @param mixed $value
* @param mixed $schema
*
* @phpstan-param int-mask-of<Constraint::CHECK_MODE_*> $checkMode
* @phpstan-return int-mask-of<Validator::ERROR_*>
*/
public function validate(&$value, $schema = null, ?int $checkMode = null): int
{
// reset errors prior to validation
$this->reset();
// set checkMode
$initialCheckMode = $this->factory->getConfig();
if ($checkMode !== null) {
$this->factory->setConfig($checkMode);
}
// add provided schema to SchemaStorage with internal URI to allow internal $ref resolution
$schemaURI = SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI;
if (LooseTypeCheck::propertyExists($schema, 'id')) {
$schemaURI = LooseTypeCheck::propertyGet($schema, 'id');
}
$this->factory->getSchemaStorage()->addSchema($schemaURI, $schema);
$validator = $this->factory->createInstanceFor('schema');
$validator->check(
$value,
$this->factory->getSchemaStorage()->getSchema($schemaURI)
);
$this->factory->setConfig($initialCheckMode);
$this->addErrors(array_unique($validator->getErrors(), SORT_REGULAR));
return $validator->getErrorMask();
}
/**
* Alias to validate(), to maintain backwards-compatibility with the previous API
*
* @deprecated since 6.0.0, use Validator::validate() instead, to be removed in 7.0
*
* @param mixed $value
* @param mixed $schema
*
* @phpstan-return int-mask-of<Validator::ERROR_*>
*/
public function check($value, $schema): int
{
return $this->validate($value, $schema);
}
/**
* Alias to validate(), to maintain backwards-compatibility with the previous API
*
* @deprecated since 6.0.0, use Validator::validate() instead, to be removed in 7.0
*
* @param mixed $value
* @param mixed $schema
*
* @phpstan-return int-mask-of<Validator::ERROR_*>
*/
public function coerce(&$value, $schema): int
{
return $this->validate($value, $schema, Constraint::CHECK_MODE_COERCE_TYPES);
}
}