Init Server Composer Components

This commit is contained in:
Eole 2016-01-21 10:29:26 +01:00
parent 35db27b0e6
commit a44cc1d2e3
177 changed files with 24745 additions and 0 deletions

View file

@ -0,0 +1,112 @@
<?php
/*
* 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;
/**
* 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, $path = null, $i = null)
{
// Verify minItems
if (isset($schema->minItems) && count($value) < $schema->minItems) {
$this->addError($path, "There must be a minimum of " . $schema->minItems . " items in the array", 'minItems', array('minItems' => $schema->minItems,));
}
// Verify maxItems
if (isset($schema->maxItems) && count($value) > $schema->maxItems) {
$this->addError($path, "There must be a maximum of " . $schema->maxItems . " items in the array", 'maxItems', array('maxItems' => $schema->maxItems,));
}
// Verify uniqueItems
if (isset($schema->uniqueItems) && $schema->uniqueItems) {
$unique = $value;
if (is_array($value) && count($value)) {
$unique = array_map(function($e) { return var_export($e, true); }, $value);
}
if (count(array_unique($unique)) != count($value)) {
$this->addError($path, "There are no duplicates allowed in the array", 'uniqueItems');
}
}
// Verify items
if (isset($schema->items)) {
$this->validateItems($value, $schema, $path, $i);
}
}
/**
* Validates the items
*
* @param array $value
* @param \stdClass $schema
* @param string $path
* @param string $i
*/
protected function validateItems($value, $schema = null, $path = null, $i = null)
{
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;
} else if (isset($secondErrors) && count($secondErrors) === count($this->getErrors())) {
$this->errors = $initErrors;
}
}
} 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(
$path, 'The item ' . $i . '[' . $k . '] is not defined and the definition does not allow additional items', 'additionalItems', array('additionalItems' => $schema->additionalItems,));
}
} else {
// Should be valid against an empty schema
$this->checkUndefined($v, new \stdClass(), $path, $k);
}
}
}
// 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++) {
$this->checkUndefined(new UndefinedConstraint(), $schema->items[$k], $path, $k);
}
}
}
}
}

View file

@ -0,0 +1,291 @@
<?php
/*
* 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\Uri\UriRetriever;
/**
* The Base Constraints, all Validators should extend this class
*
* @author Robert Schönthal <seroscho@googlemail.com>
* @author Bruno Prieto Reis <bruno.p.reis@gmail.com>
*/
abstract class Constraint implements ConstraintInterface
{
protected $checkMode = self::CHECK_MODE_NORMAL;
protected $uriRetriever;
protected $errors = array();
protected $inlineSchemaProperty = '$schema';
const CHECK_MODE_NORMAL = 1;
const CHECK_MODE_TYPE_CAST = 2;
/**
* @var null|Factory
*/
private $factory;
/**
* @param int $checkMode
* @param UriRetriever $uriRetriever
* @param Factory $factory
*/
public function __construct($checkMode = self::CHECK_MODE_NORMAL, UriRetriever $uriRetriever = null, Factory $factory = null)
{
$this->checkMode = $checkMode;
$this->uriRetriever = $uriRetriever;
$this->factory = $factory;
}
/**
* @return UriRetriever $uriRetriever
*/
public function getUriRetriever()
{
if (is_null($this->uriRetriever))
{
$this->setUriRetriever(new UriRetriever);
}
return $this->uriRetriever;
}
/**
* @return Factory
*/
public function getFactory()
{
if (!$this->factory) {
$this->factory = new Factory($this->getUriRetriever());
}
return $this->factory;
}
/**
* @param UriRetriever $uriRetriever
*/
public function setUriRetriever(UriRetriever $uriRetriever)
{
$this->uriRetriever = $uriRetriever;
}
/**
* {@inheritDoc}
*/
public function addError($path, $message, $constraint='', array $more=null)
{
$error = array(
'property' => $path,
'message' => $message,
'constraint' => $constraint,
);
if (is_array($more) && count($more) > 0)
{
$error += $more;
}
$this->errors[] = $error;
}
/**
* {@inheritDoc}
*/
public function addErrors(array $errors)
{
$this->errors = array_merge($this->errors, $errors);
}
/**
* {@inheritDoc}
*/
public function getErrors()
{
return $this->errors;
}
/**
* {@inheritDoc}
*/
public function isValid()
{
return !$this->getErrors();
}
/**
* Clears any reported errors. Should be used between
* multiple validation checks.
*/
public function reset()
{
$this->errors = array();
}
/**
* Bubble down the path
*
* @param string $path Current path
* @param mixed $i What to append to the path
*
* @return string
*/
protected function incrementPath($path, $i)
{
if ($path !== '') {
if (is_int($i)) {
$path .= '[' . $i . ']';
} elseif ($i == '') {
$path .= '';
} else {
$path .= '.' . $i;
}
} else {
$path = $i;
}
return $path;
}
/**
* Validates an array
*
* @param mixed $value
* @param mixed $schema
* @param mixed $path
* @param mixed $i
*/
protected function checkArray($value, $schema = null, $path = null, $i = null)
{
$validator = $this->getFactory()->createInstanceFor('collection');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* Validates an object
*
* @param mixed $value
* @param mixed $schema
* @param mixed $path
* @param mixed $i
* @param mixed $patternProperties
*/
protected function checkObject($value, $schema = null, $path = null, $i = null, $patternProperties = null)
{
$validator = $this->getFactory()->createInstanceFor('object');
$validator->check($value, $schema, $path, $i, $patternProperties);
$this->addErrors($validator->getErrors());
}
/**
* Validates the type of a property
*
* @param mixed $value
* @param mixed $schema
* @param mixed $path
* @param mixed $i
*/
protected function checkType($value, $schema = null, $path = null, $i = null)
{
$validator = $this->getFactory()->createInstanceFor('type');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* Checks a undefined element
*
* @param mixed $value
* @param mixed $schema
* @param mixed $path
* @param mixed $i
*/
protected function checkUndefined($value, $schema = null, $path = null, $i = null)
{
$validator = $this->getFactory()->createInstanceFor('undefined');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* Checks a string element
*
* @param mixed $value
* @param mixed $schema
* @param mixed $path
* @param mixed $i
*/
protected function checkString($value, $schema = null, $path = null, $i = null)
{
$validator = $this->getFactory()->createInstanceFor('string');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* Checks a number element
*
* @param mixed $value
* @param mixed $schema
* @param mixed $path
* @param mixed $i
*/
protected function checkNumber($value, $schema = null, $path = null, $i = null)
{
$validator = $this->getFactory()->createInstanceFor('number');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* Checks a enum element
*
* @param mixed $value
* @param mixed $schema
* @param mixed $path
* @param mixed $i
*/
protected function checkEnum($value, $schema = null, $path = null, $i = null)
{
$validator = $this->getFactory()->createInstanceFor('enum');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
protected function checkFormat($value, $schema = null, $path = null, $i = null)
{
$validator = $this->getFactory()->createInstanceFor('format');
$validator->check($value, $schema, $path, $i);
$this->addErrors($validator->getErrors());
}
/**
* @param string $uri JSON Schema URI
* @return string JSON Schema contents
*/
protected function retrieveUri($uri)
{
if (null === $this->uriRetriever) {
$this->setUriRetriever(new UriRetriever);
}
$jsonSchema = $this->uriRetriever->retrieve($uri);
// TODO validate using schema
return $jsonSchema;
}
}

View file

@ -0,0 +1,60 @@
<?php
/*
* 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;
/**
* The Constraints Interface
*
* @author Robert Schönthal <seroscho@googlemail.com>
*/
interface ConstraintInterface
{
/**
* returns all collected errors
*
* @return array
*/
public function getErrors();
/**
* adds errors to this validator
*
* @param array $errors
*/
public function addErrors(array $errors);
/**
* adds an error
*
* @param string $path
* @param string $message
* @param string $constraint the constraint/rule that is broken, e.g.: 'minLength'
* @param array $more more array elements to add to the error
*/
public function addError($path, $message, $constraint='', array $more=null);
/**
* checks if the validator has not raised errors
*
* @return boolean
*/
public function isValid();
/**
* invokes the validation of an element
*
* @abstract
* @param mixed $value
* @param mixed $schema
* @param mixed $path
* @param mixed $i
*/
public function check($value, $schema = null, $path = null, $i = null);
}

View file

@ -0,0 +1,46 @@
<?php
/*
* 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;
/**
* 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, $path = null, $i = null)
{
// Only validate enum if the attribute exists
if ($element instanceof UndefinedConstraint && (!isset($schema->required) || !$schema->required)) {
return;
}
foreach ($schema->enum as $enum) {
$type = gettype($element);
if ($type === gettype($enum)) {
if ($type == "object") {
if ($element == $enum)
return;
} else {
if ($element === $enum)
return;
}
}
}
$this->addError($path, "Does not have a value in the enumeration " . print_r($schema->enum, true), 'enum', array('enum' => $schema->enum,));
}
}

View file

@ -0,0 +1,81 @@
<?php
/*
* 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\Uri\UriRetriever;
use JsonSchema\Validator;
/**
* Factory for centralize constraint initialization.
*/
class Factory
{
/**
* @var UriRetriever
*/
protected $uriRetriever;
/**
* @param UriRetriever $uriRetriever
*/
public function __construct(UriRetriever $uriRetriever = null)
{
if (!$uriRetriever) {
$uriRetriever = new UriRetriever();
}
$this->uriRetriever = $uriRetriever;
}
/**
* @return UriRetriever
*/
public function getUriRetriever()
{
return $this->uriRetriever;
}
/**
* Create a constraint instance for the given constraint name.
*
* @param string $constraintName
* @return ConstraintInterface|ObjectConstraint
* @throws InvalidArgumentException if is not possible create the constraint instance.
*/
public function createInstanceFor($constraintName)
{
switch ($constraintName) {
case 'array':
case 'collection':
return new CollectionConstraint(Constraint::CHECK_MODE_NORMAL, $this->uriRetriever, $this);
case 'object':
return new ObjectConstraint(Constraint::CHECK_MODE_NORMAL, $this->uriRetriever, $this);
case 'type':
return new TypeConstraint(Constraint::CHECK_MODE_NORMAL, $this->uriRetriever, $this);
case 'undefined':
return new UndefinedConstraint(Constraint::CHECK_MODE_NORMAL, $this->uriRetriever, $this);
case 'string':
return new StringConstraint(Constraint::CHECK_MODE_NORMAL, $this->uriRetriever, $this);
case 'number':
return new NumberConstraint(Constraint::CHECK_MODE_NORMAL, $this->uriRetriever, $this);
case 'enum':
return new EnumConstraint(Constraint::CHECK_MODE_NORMAL, $this->uriRetriever, $this);
case 'format':
return new FormatConstraint(Constraint::CHECK_MODE_NORMAL, $this->uriRetriever, $this);
case 'schema':
return new SchemaConstraint(Constraint::CHECK_MODE_NORMAL, $this->uriRetriever, $this);
case 'validator':
return new Validator(Constraint::CHECK_MODE_NORMAL, $this->uriRetriever, $this);
}
throw new InvalidArgumentException('Unknown constraint ' . $constraintName);
}
}

View file

@ -0,0 +1,181 @@
<?php
/*
* 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;
/**
* Validates against the "format" property
*
* @author Justin Rainbow <justin.rainbow@gmail.com>
* @link http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.23
*/
class FormatConstraint extends Constraint
{
/**
* {@inheritDoc}
*/
public function check($element, $schema = null, $path = null, $i = null)
{
if (!isset($schema->format)) {
return;
}
switch ($schema->format) {
case 'date':
if (!$date = $this->validateDateTime($element, 'Y-m-d')) {
$this->addError($path, sprintf('Invalid date %s, expected format YYYY-MM-DD', json_encode($element)), 'format', array('format' => $schema->format,));
}
break;
case 'time':
if (!$this->validateDateTime($element, 'H:i:s')) {
$this->addError($path, sprintf('Invalid time %s, expected format hh:mm:ss', json_encode($element)), 'format', array('format' => $schema->format,));
}
break;
case 'date-time':
if (!$this->validateDateTime($element, 'Y-m-d\TH:i:s\Z') &&
!$this->validateDateTime($element, 'Y-m-d\TH:i:s.u\Z') &&
!$this->validateDateTime($element, 'Y-m-d\TH:i:sP') &&
!$this->validateDateTime($element, 'Y-m-d\TH:i:sO')
) {
$this->addError($path, sprintf('Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm', json_encode($element)), 'format', array('format' => $schema->format,));
}
break;
case 'utc-millisec':
if (!$this->validateDateTime($element, 'U')) {
$this->addError($path, sprintf('Invalid time %s, expected integer of milliseconds since Epoch', json_encode($element)), 'format', array('format' => $schema->format,));
}
break;
case 'regex':
if (!$this->validateRegex($element)) {
$this->addError($path, 'Invalid regex format ' . $element, 'format', array('format' => $schema->format,));
}
break;
case 'color':
if (!$this->validateColor($element)) {
$this->addError($path, "Invalid color", 'format', array('format' => $schema->format,));
}
break;
case 'style':
if (!$this->validateStyle($element)) {
$this->addError($path, "Invalid style", 'format', array('format' => $schema->format,));
}
break;
case 'phone':
if (!$this->validatePhone($element)) {
$this->addError($path, "Invalid phone number", 'format', array('format' => $schema->format,));
}
break;
case 'uri':
if (null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) {
$this->addError($path, "Invalid URL format", 'format', array('format' => $schema->format,));
}
break;
case 'email':
if (null === filter_var($element, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE)) {
$this->addError($path, "Invalid email", 'format', array('format' => $schema->format,));
}
break;
case 'ip-address':
case 'ipv4':
if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) {
$this->addError($path, "Invalid IP address", 'format', array('format' => $schema->format,));
}
break;
case 'ipv6':
if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) {
$this->addError($path, "Invalid IP address", 'format', array('format' => $schema->format,));
}
break;
case 'host-name':
case 'hostname':
if (!$this->validateHostname($element)) {
$this->addError($path, "Invalid hostname", 'format', array('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, $datetime);
if (!$dt) {
return false;
}
if ($datetime === $dt->format($format)) {
return true;
}
// handles the case where a non-6 digit microsecond datetime is passed
// which will fail the above string comparison because the passed
// $datetime may be '2000-05-01T12:12:12.123Z' but format() will return
// '2000-05-01T12:12:12.123000Z'
if ((strpos('u', $format) !== -1) && (intval($dt->format('u')) > 0)) {
return true;
}
return false;
}
protected function validateRegex($regex)
{
return false !== @preg_match('/' . $regex . '/', '');
}
protected function validateColor($color)
{
if (in_array(strtolower($color), array('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)
{
return preg_match('/^[_a-z]+\.([_a-z]+\.?)+$/i', $host);
}
}

View file

@ -0,0 +1,83 @@
<?php
/*
* 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;
/**
* 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, $path = null, $i = null)
{
// Verify minimum
if (isset($schema->exclusiveMinimum)) {
if (isset($schema->minimum)) {
if ($schema->exclusiveMinimum && $element === $schema->minimum) {
$this->addError($path, "Must have a minimum value greater than boundary value of " . $schema->minimum, 'exclusiveMinimum', array('minimum' => $schema->minimum,));
} else if ($element < $schema->minimum) {
$this->addError($path, "Must have a minimum value of " . $schema->minimum, 'minimum', array('minimum' => $schema->minimum,));
}
} else {
$this->addError($path, "Use of exclusiveMinimum requires presence of minimum", 'missingMinimum');
}
} else if (isset($schema->minimum) && $element < $schema->minimum) {
$this->addError($path, "Must have a minimum value of " . $schema->minimum, 'minimum', array('minimum' => $schema->minimum,));
}
// Verify maximum
if (isset($schema->exclusiveMaximum)) {
if (isset($schema->maximum)) {
if ($schema->exclusiveMaximum && $element === $schema->maximum) {
$this->addError($path, "Must have a maximum value less than boundary value of " . $schema->maximum, 'exclusiveMaximum', array('maximum' => $schema->maximum,));
} else if ($element > $schema->maximum) {
$this->addError($path, "Must have a maximum value of " . $schema->maximum, 'maximum', array('maximum' => $schema->maximum,));
}
} else {
$this->addError($path, "Use of exclusiveMaximum requires presence of maximum", 'missingMinimum');
}
} else if (isset($schema->maximum) && $element > $schema->maximum) {
$this->addError($path, "Must have a maximum value of " . $schema->maximum, 'maximum', array('maximum' => $schema->maximum,));
}
// Verify divisibleBy - Draft v3
if (isset($schema->divisibleBy) && $this->fmod($element, $schema->divisibleBy) != 0) {
$this->addError($path, "Is not divisible by " . $schema->divisibleBy, 'divisibleBy', array('divisibleBy' => $schema->divisibleBy,));
}
// Verify multipleOf - Draft v4
if (isset($schema->multipleOf) && $this->fmod($element, $schema->multipleOf) != 0) {
$this->addError($path, "Must be a multiple of " . $schema->multipleOf, 'multipleOf', array('multipleOf' => $schema->multipleOf,));
}
$this->checkFormat($element, $schema, $path, $i);
}
private function fmod($number1, $number2)
{
$modulus = fmod($number1, $number2);
$precision = abs(0.0000000001);
$diff = (float)($modulus - $number2);
if (-$precision < $diff && $diff < $precision) {
return 0.0;
}
$decimals1 = mb_strpos($number1, ".") ? mb_strlen($number1) - mb_strpos($number1, ".") - 1 : 0;
$decimals2 = mb_strpos($number2, ".") ? mb_strlen($number2) - mb_strpos($number2, ".") - 1 : 0;
return (float)round($modulus, max($decimals1, $decimals2));
}
}

View file

@ -0,0 +1,149 @@
<?php
/*
* 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;
/**
* The ObjectConstraint Constraints, validates an object against a given schema
*
* @author Robert Schönthal <seroscho@googlemail.com>
* @author Bruno Prieto Reis <bruno.p.reis@gmail.com>
*/
class ObjectConstraint extends Constraint
{
/**
* {@inheritDoc}
*/
function check($element, $definition = null, $path = null, $additionalProp = null, $patternProperties = null)
{
if ($element instanceof UndefinedConstraint) {
return;
}
$matches = array();
if ($patternProperties) {
$matches = $this->validatePatternProperties($element, $path, $patternProperties);
}
if ($definition) {
// validate the definition properties
$this->validateDefinition($element, $definition, $path);
}
// additional the element properties
$this->validateElement($element, $matches, $definition, $path, $additionalProp);
}
public function validatePatternProperties($element, $path, $patternProperties)
{
$try = array('/','#','+','~','%');
$matches = array();
foreach ($patternProperties as $pregex => $schema) {
$delimiter = '/';
// Choose delimiter. Necessary for patterns like ^/ , otherwise you get error
foreach ($try as $delimiter) {
if (strpos($pregex, $delimiter) === false) { // safe to use
break;
}
}
// Validate the pattern before using it to test for matches
if (@preg_match($delimiter. $pregex . $delimiter, '') === false) {
$this->addError($path, 'The pattern "' . $pregex . '" is invalid', 'pregex', array('pregex' => $pregex,));
continue;
}
foreach ($element as $i => $value) {
if (preg_match($delimiter . $pregex . $delimiter, $i)) {
$matches[] = $i;
$this->checkUndefined($value, $schema ? : new \stdClass(), $path, $i);
}
}
}
return $matches;
}
/**
* Validates the element properties
*
* @param \stdClass $element Element to validate
* @param array $matches Matches from patternProperties (if any)
* @param \stdClass $objectDefinition ObjectConstraint definition
* @param string $path Path to test?
* @param mixed $additionalProp Additional properties
*/
public function validateElement($element, $matches, $objectDefinition = null, $path = null, $additionalProp = null)
{
foreach ($element as $i => $value) {
$property = $this->getProperty($element, $i, new UndefinedConstraint());
$definition = $this->getProperty($objectDefinition, $i);
// no additional properties allowed
if (!in_array($i, $matches) && $additionalProp === false && $this->inlineSchemaProperty !== $i && !$definition) {
$this->addError($path, "The property " . $i . " is not defined and the definition does not allow additional properties", 'additionalProp');
}
// additional properties defined
if (!in_array($i, $matches) && $additionalProp && !$definition) {
if ($additionalProp === true) {
$this->checkUndefined($value, null, $path, $i);
} else {
$this->checkUndefined($value, $additionalProp, $path, $i);
}
}
// property requires presence of another
$require = $this->getProperty($definition, 'requires');
if ($require && !$this->getProperty($element, $require)) {
$this->addError($path, "The presence of the property " . $i . " requires that " . $require . " also be present", 'requires');
}
if (!$definition) {
// normal property verification
$this->checkUndefined($value, new \stdClass(), $path, $i);
}
}
}
/**
* Validates the definition properties
*
* @param \stdClass $element Element to validate
* @param \stdClass $objectDefinition ObjectConstraint definition
* @param string $path Path?
*/
public function validateDefinition($element, $objectDefinition = null, $path = null)
{
foreach ($objectDefinition as $i => $value) {
$property = $this->getProperty($element, $i, new UndefinedConstraint());
$definition = $this->getProperty($objectDefinition, $i);
$this->checkUndefined($property, $definition, $path, $i);
}
}
/**
* 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) /*$this->checkMode == self::CHECK_MODE_TYPE_CAST*/) {
return array_key_exists($property, $element) ? $element[$property] : $fallback;
} elseif (is_object($element)) {
return property_exists($element, $property) ? $element->$property : $fallback;
}
return $fallback;
}
}

View file

@ -0,0 +1,37 @@
<?php
/*
* 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;
/**
* 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
{
/**
* {@inheritDoc}
*/
public function check($element, $schema = null, $path = null, $i = null)
{
if ($schema !== null) {
// passed schema
$this->checkUndefined($element, $schema, '', '');
} elseif (property_exists($element, $this->inlineSchemaProperty)) {
// inline schema
$this->checkUndefined($element, $element->{$this->inlineSchemaProperty}, '', '');
} else {
throw new InvalidArgumentException('no schema found to verify against');
}
}
}

View file

@ -0,0 +1,57 @@
<?php
/*
* 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;
/**
* 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, $path = null, $i = null)
{
// Verify maxLength
if (isset($schema->maxLength) && $this->strlen($element) > $schema->maxLength) {
$this->addError($path, "Must be at most " . $schema->maxLength . " characters long", 'maxLength', array(
'maxLength' => $schema->maxLength,
));
}
//verify minLength
if (isset($schema->minLength) && $this->strlen($element) < $schema->minLength) {
$this->addError($path, "Must be at least " . $schema->minLength . " characters long", 'minLength', array(
'minLength' => $schema->minLength,
));
}
// Verify a regex pattern
if (isset($schema->pattern) && !preg_match('#' . str_replace('#', '\\#', $schema->pattern) . '#', $element)) {
$this->addError($path, "Does not match the regex pattern " . $schema->pattern, 'pattern', array(
'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));
} else {
return strlen($string);
}
}
}

View file

@ -0,0 +1,145 @@
<?php
/*
* 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 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
*/
static $wording = array(
'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, $path = null, $i = null)
{
$type = isset($schema->type) ? $schema->type : null;
$isValid = true;
if (is_array($type)) {
// @TODO refactor
$validatedOneType = false;
$errors = array();
foreach ($type as $tp) {
$validator = new TypeConstraint($this->checkMode);
$subSchema = new \stdClass();
$subSchema->type = $tp;
$validator->check($value, $subSchema, $path, null);
$error = $validator->getErrors();
if (!count($error)) {
$validatedOneType = true;
break;
}
$errors = $error;
}
if (!$validatedOneType) {
$this->addErrors($errors);
return;
}
} elseif (is_object($type)) {
$this->checkUndefined($value, $type, $path);
} else {
$isValid = $this->validateType($value, $type);
}
if ($isValid === false) {
if (!isset(self::$wording[$type])) {
throw new StandardUnexpectedValueException(
sprintf(
"No wording for %s available, expected wordings are: [%s]",
var_export($type, true),
implode(', ', array_filter(self::$wording)))
);
}
$this->addError($path, ucwords(gettype($value)) . " value found, but " . self::$wording[$type] . " is required", 'type');
}
}
/**
* Verifies that a given value is of a certain type
*
* @param mixed $value Value to validate
* @param string $type TypeConstraint to check against
*
* @return boolean
*
* @throws InvalidArgumentException
*/
protected function validateType($value, $type)
{
//mostly the case for inline schema
if (!$type) {
return true;
}
if ('integer' === $type) {
return is_int($value);
}
if ('number' === $type) {
return is_numeric($value) && !is_string($value);
}
if ('boolean' === $type) {
return is_bool($value);
}
if ('object' === $type) {
return is_object($value);
//return ($this::CHECK_MODE_TYPE_CAST == $this->checkMode) ? is_array($value) : is_object($value);
}
if ('array' === $type) {
return is_array($value);
}
if ('string' === $type) {
return is_string($value);
}
if ('email' === $type) {
return is_string($value);
}
if ('null' === $type) {
return is_null($value);
}
if ('any' === $type) {
return true;
}
throw new InvalidArgumentException((is_object($value) ? 'object' : $value) . ' is an invalid type for ' . $type);
}
}

View file

@ -0,0 +1,307 @@
<?php
/*
* 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\Uri\UriResolver;
/**
* The UndefinedConstraint Constraints
*
* @author Robert Schönthal <seroscho@googlemail.com>
* @author Bruno Prieto Reis <bruno.p.reis@gmail.com>
*/
class UndefinedConstraint extends Constraint
{
/**
* {@inheritDoc}
*/
public function check($value, $schema = null, $path = null, $i = null)
{
if (is_null($schema)) {
return;
}
if (!is_object($schema)) {
throw new InvalidArgumentException(
'Given schema must be an object in ' . $path
. ' but is a ' . gettype($schema)
);
}
$i = is_null($i) ? "" : $i;
$path = $this->incrementPath($path, $i);
// check special properties
$this->validateCommonProperties($value, $schema, $path);
// 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 string $path
* @param string $i
*/
public function validateTypes($value, $schema = null, $path = null, $i = null)
{
// check array
if (is_array($value)) {
$this->checkArray($value, $schema, $path, $i);
}
// check object
if (is_object($value) && (isset($schema->properties) || isset($schema->patternProperties) || isset($schema->additionalProperties))) {
$this->checkObject(
$value,
isset($schema->properties) ? $schema->properties : null,
$path,
isset($schema->additionalProperties) ? $schema->additionalProperties : null,
isset($schema->patternProperties) ? $schema->patternProperties : null
);
}
// 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);
}
}
/**
* Validates common properties
*
* @param mixed $value
* @param mixed $schema
* @param string $path
* @param string $i
*/
protected function validateCommonProperties($value, $schema = null, $path = null, $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);
}
}
// Verify required values
if (is_object($value)) {
if (!($value instanceof UndefinedConstraint) && 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 (!property_exists($value, $required)) {
$this->addError((!$path) ? $required : "$path.$required", "The property " . $required . " is required", 'required');
}
}
} else if (isset($schema->required) && !is_array($schema->required)) {
// Draft 3 - Required attribute - e.g. "foo": {"type": "string", "required": true}
if ( $schema->required && $value instanceof UndefinedConstraint) {
$this->addError($path, "Is missing and it is required", 'required');
}
}
}
// Verify type
if (!($value instanceof UndefinedConstraint)) {
$this->checkType($value, $schema, $path);
}
// 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($path, "Disallowed value was matched", 'disallow');
} 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($path, "Matched a schema which it should not", 'not');
} else {
$this->errors = $initErrors;
}
}
// Verify minimum and maximum number of properties
if (is_object($value)) {
if (isset($schema->minProperties)) {
if (count(get_object_vars($value)) < $schema->minProperties) {
$this->addError($path, "Must contain a minimum of " . $schema->minProperties . " properties", 'minProperties', array('minProperties' => $schema->minProperties,));
}
}
if (isset($schema->maxProperties)) {
if (count(get_object_vars($value)) > $schema->maxProperties) {
$this->addError($path, "Must contain no more than " . $schema->maxProperties . " properties", 'maxProperties', array('maxProperties' => $schema->maxProperties,));
}
}
}
// Verify that dependencies are met
if (is_object($value) && isset($schema->dependencies)) {
$this->validateDependencies($value, $schema->dependencies, $path);
}
}
/**
* Validate allOf, anyOf, and oneOf properties
*
* @param mixed $value
* @param mixed $schema
* @param string $path
* @param string $i
*/
protected function validateOfProperties($value, $schema, $path, $i = "")
{
// Verify type
if ($value instanceof UndefinedConstraint) {
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($path, "Failed to match all schemas", 'allOf');
}
}
if (isset($schema->anyOf)) {
$isValid = false;
$startErrors = $this->getErrors();
foreach ($schema->anyOf as $anyOf) {
$initErrors = $this->getErrors();
$this->checkUndefined($value, $anyOf, $path, $i);
if ($isValid = (count($this->getErrors()) == count($initErrors))) {
break;
}
}
if (!$isValid) {
$this->addError($path, "Failed to match at least one schema", 'anyOf');
} else {
$this->errors = $startErrors;
}
}
if (isset($schema->oneOf)) {
$allErrors = array();
$matchedSchemas = 0;
$startErrors = $this->getErrors();
foreach ($schema->oneOf as $oneOf) {
$this->errors = array();
$this->checkUndefined($value, $oneOf, $path, $i);
if (count($this->getErrors()) == 0) {
$matchedSchemas++;
}
$allErrors = array_merge($allErrors, array_values($this->getErrors()));
}
if ($matchedSchemas !== 1) {
$this->addErrors(
array_merge(
$allErrors,
array(array(
'property' => $path,
'message' => "Failed to match exactly one schema",
'constraint' => 'oneOf',
),),
$startErrors
)
);
} else {
$this->errors = $startErrors;
}
}
}
/**
* Validate dependencies
*
* @param mixed $value
* @param mixed $dependencies
* @param string $path
* @param string $i
*/
protected function validateDependencies($value, $dependencies, $path, $i = "")
{
foreach ($dependencies as $key => $dependency) {
if (property_exists($value, $key)) {
if (is_string($dependency)) {
// Draft 3 string is allowed - e.g. "dependencies": {"bar": "foo"}
if (!property_exists($value, $dependency)) {
$this->addError($path, "$key depends on $dependency and $dependency is missing", 'dependencies');
}
} else if (is_array($dependency)) {
// Draft 4 must be an array - e.g. "dependencies": {"bar": ["foo"]}
foreach ($dependency as $d) {
if (!property_exists($value, $d)) {
$this->addError($path, "$key depends on $d and $d is missing", 'dependencies');
}
}
} else if (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->getUriRetriever();
$jsonSchema = null;
if ($resolver->isValid($schemaUri)) {
$schemaId = property_exists($schema, 'id') ? $schema->id : null;
$jsonSchema = $retriever->retrieve($schemaId, $schemaUri);
}
return $jsonSchema;
}
}

View file

@ -0,0 +1,17 @@
<?php
/*
* 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
{
}

View file

@ -0,0 +1,17 @@
<?php
/*
* 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
{
}

View file

@ -0,0 +1,17 @@
<?php
/*
* 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
{
}

View file

@ -0,0 +1,40 @@
<?php
/*
* 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);
}
}

View file

@ -0,0 +1,17 @@
<?php
/*
* 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
{
}

View file

@ -0,0 +1,17 @@
<?php
/*
* 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
{
}

View file

@ -0,0 +1,277 @@
<?php
/*
* 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\Exception\JsonDecodingException;
use JsonSchema\Uri\Retrievers\UriRetrieverInterface;
use JsonSchema\Uri\UriRetriever;
/**
* Take in an object that's a JSON schema and take care of all $ref references
*
* @author Tyler Akins <fidian@rumkin.com>
* @see README.md
*/
class RefResolver
{
/**
* HACK to prevent too many recursive expansions.
* Happens e.g. when you want to validate a schema against the schema
* definition.
*
* @var integer
*/
protected static $depth = 0;
/**
* maximum references depth
* @var integer
*/
public static $maxDepth = 7;
/**
* @var UriRetrieverInterface
*/
protected $uriRetriever = null;
/**
* @var object
*/
protected $rootSchema = null;
/**
* @param UriRetriever $retriever
*/
public function __construct($retriever = null)
{
$this->uriRetriever = $retriever;
}
/**
* Retrieves a given schema given a ref and a source URI
*
* @param string $ref Reference from schema
* @param string $sourceUri URI where original schema was located
* @return object Schema
*/
public function fetchRef($ref, $sourceUri)
{
$retriever = $this->getUriRetriever();
$jsonSchema = $retriever->retrieve($ref, $sourceUri);
$this->resolve($jsonSchema);
return $jsonSchema;
}
/**
* Return the URI Retriever, defaulting to making a new one if one
* was not yet set.
*
* @return UriRetriever
*/
public function getUriRetriever()
{
if (is_null($this->uriRetriever)) {
$this->setUriRetriever(new UriRetriever);
}
return $this->uriRetriever;
}
/**
* Resolves all $ref references for a given schema. Recurses through
* the object to resolve references of any child schemas.
*
* The 'format' property is omitted because it isn't required for
* validation. Theoretically, this class could be extended to look
* for URIs in formats: "These custom formats MAY be expressed as
* an URI, and this URI MAY reference a schema of that format."
*
* The 'id' property is not filled in, but that could be made to happen.
*
* @param object $schema JSON Schema to flesh out
* @param string $sourceUri URI where this schema was located
*/
public function resolve($schema, $sourceUri = null)
{
if (self::$depth > self::$maxDepth) {
self::$depth = 0;
throw new JsonDecodingException(JSON_ERROR_DEPTH);
}
++self::$depth;
if (! is_object($schema)) {
--self::$depth;
return;
}
if (null === $sourceUri && ! empty($schema->id)) {
$sourceUri = $schema->id;
}
if (null === $this->rootSchema) {
$this->rootSchema = $schema;
}
// Resolve $ref first
$this->resolveRef($schema, $sourceUri);
// These properties are just schemas
// eg. items can be a schema or an array of schemas
foreach (array('additionalItems', 'additionalProperties', 'extends', 'items') as $propertyName) {
$this->resolveProperty($schema, $propertyName, $sourceUri);
}
// These are all potentially arrays that contain schema objects
// eg. type can be a value or an array of values/schemas
// eg. items can be a schema or an array of schemas
foreach (array('disallow', 'extends', 'items', 'type', 'allOf', 'anyOf', 'oneOf') as $propertyName) {
$this->resolveArrayOfSchemas($schema, $propertyName, $sourceUri);
}
// These are all objects containing properties whose values are schemas
foreach (array('dependencies', 'patternProperties', 'properties') as $propertyName) {
$this->resolveObjectOfSchemas($schema, $propertyName, $sourceUri);
}
--self::$depth;
}
/**
* Given an object and a property name, that property should be an
* array whose values can be schemas.
*
* @param object $schema JSON Schema to flesh out
* @param string $propertyName Property to work on
* @param string $sourceUri URI where this schema was located
*/
public function resolveArrayOfSchemas($schema, $propertyName, $sourceUri)
{
if (! isset($schema->$propertyName) || ! is_array($schema->$propertyName)) {
return;
}
foreach ($schema->$propertyName as $possiblySchema) {
$this->resolve($possiblySchema, $sourceUri);
}
}
/**
* Given an object and a property name, that property should be an
* object whose properties are schema objects.
*
* @param object $schema JSON Schema to flesh out
* @param string $propertyName Property to work on
* @param string $sourceUri URI where this schema was located
*/
public function resolveObjectOfSchemas($schema, $propertyName, $sourceUri)
{
if (! isset($schema->$propertyName) || ! is_object($schema->$propertyName)) {
return;
}
foreach (get_object_vars($schema->$propertyName) as $possiblySchema) {
$this->resolve($possiblySchema, $sourceUri);
}
}
/**
* Given an object and a property name, that property should be a
* schema object.
*
* @param object $schema JSON Schema to flesh out
* @param string $propertyName Property to work on
* @param string $sourceUri URI where this schema was located
*/
public function resolveProperty($schema, $propertyName, $sourceUri)
{
if (! isset($schema->$propertyName)) {
return;
}
$this->resolve($schema->$propertyName, $sourceUri);
}
/**
* Look for the $ref property in the object. If found, remove the
* reference and augment this object with the contents of another
* schema.
*
* @param object $schema JSON Schema to flesh out
* @param string $sourceUri URI where this schema was located
*/
public function resolveRef($schema, $sourceUri)
{
$ref = '$ref';
if (empty($schema->$ref)) {
return;
}
$splitRef = explode('#', $schema->$ref, 2);
$refDoc = $splitRef[0];
$refPath = null;
if (count($splitRef) === 2) {
$refPath = explode('/', $splitRef[1]);
array_shift($refPath);
}
if (empty($refDoc) && empty($refPath)) {
// TODO: Not yet implemented - root pointer ref, causes recursion issues
return;
}
if (!empty($refDoc)) {
$refSchema = $this->fetchRef($refDoc, $sourceUri);
} else {
$refSchema = $this->rootSchema;
}
if (null !== $refPath) {
$refSchema = $this->resolveRefSegment($refSchema, $refPath);
}
unset($schema->$ref);
// Augment the current $schema object with properties fetched
foreach (get_object_vars($refSchema) as $prop => $value) {
$schema->$prop = $value;
}
}
/**
* Set URI Retriever for use with the Ref Resolver
*
* @param UriRetriever $retriever
* @return $this for chaining
*/
public function setUriRetriever(UriRetriever $retriever)
{
$this->uriRetriever = $retriever;
return $this;
}
protected function resolveRefSegment($data, $pathParts)
{
foreach ($pathParts as $path) {
$path = strtr($path, array('~1' => '/', '~0' => '~', '%25' => '%'));
if (is_array($data)) {
$data = $data[$path];
} else {
$data = $data->{$path};
}
}
return $data;
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* JsonSchema
* @filesource
*/
namespace JsonSchema\Uri\Retrievers;
/**
* AbstractRetriever implements the default shared behavior
* that all decendant 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;
}
}

View file

@ -0,0 +1,79 @@
<?php
/*
* 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\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')) {
throw new \RuntimeException("cURL not installed");
}
}
/**
* {@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, array('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 boolean 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;
}
}

View file

@ -0,0 +1,87 @@
<?php
/*
* 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;
use JsonSchema\Validator;
/**
* 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)
{
$context = stream_context_create(array(
'http' => array(
'method' => 'GET',
'header' => "Accept: " . Validator::SCHEMA_MEDIA_TYPE
)));
set_error_handler(function() use ($uri) {
throw new ResourceNotFoundException('JSON schema not found at ' . $uri);
});
$response = file_get_contents($uri);
restore_error_handler();
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)) {
$this->fetchContentType($http_response_header);
} else {
// 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 boolean Whether the Content-Type header was found or not
*/
private function fetchContentType(array $headers)
{
foreach ($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]);
}
}
}

View file

@ -0,0 +1,54 @@
<?php
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];
}
}

View file

@ -0,0 +1,32 @@
<?php
/*
* 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();
}

View file

@ -0,0 +1,157 @@
<?php
/*
* 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;
/**
* Resolves JSON Schema URIs
*
* @author Sander Coolen <sander@jibber.nl>
*/
class UriResolver
{
/**
* Parses a URI into five main components
*
* @param string $uri
* @return array
*/
public function parse($uri)
{
preg_match('|^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?|', $uri, $match);
$components = array();
if (5 < count($match)) {
$components = array(
'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 Absolute URI
*/
public function resolve($uri, $baseUri = null)
{
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
* @return string Merged path
* @throws UriResolverException
*/
public static function combineRelativePathWithBasePath($relativePath, $basePath)
{
$relativePath = self::normalizePath($relativePath);
if ($relativePath == '') {
return $basePath;
}
if ($relativePath{0} == '/') {
return $relativePath;
}
$basePathSegments = explode('/', $basePath);
preg_match('|^/?(\.\./(?:\./)*)*|', $relativePath, $match);
$numLevelUp = strlen($match[0]) /3 + 1;
if ($numLevelUp >= count($basePathSegments)) {
throw new UriResolverException(sprintf("Unable to resolve URI '%s' from base '%s'", $relativePath, $basePath));
}
$basePathSegments = array_slice($basePathSegments, 0, -$numLevelUp);
$path = preg_replace('|^/?(\.\./(\./)*)*|', '', $relativePath);
return implode('/', $basePathSegments) . '/' . $path;
}
/**
* 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 boolean
*/
public function isValid($uri)
{
$components = $this->parse($uri);
return !empty($components);
}
}

View file

@ -0,0 +1,289 @@
<?php
/*
* 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\Uri\Retrievers\FileGetContents;
use JsonSchema\Uri\Retrievers\UriRetrieverInterface;
use JsonSchema\Validator;
use JsonSchema\Exception\InvalidSchemaMediaTypeException;
use JsonSchema\Exception\JsonDecodingException;
use JsonSchema\Exception\ResourceNotFoundException;
/**
* Retrieves JSON Schema URIs
*
* @author Tyler Akins <fidian@rumkin.com>
*/
class UriRetriever
{
/**
* @var null|UriRetrieverInterface
*/
protected $uriRetriever = null;
/**
* @var array|object[]
* @see loadSchema
*/
private $schemaCache = array();
/**
* 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 (Validator::SCHEMA_MEDIA_TYPE === $contentType) {
return;
}
if (substr($uri, 0, 23) == 'http://json-schema.org/') {
//HACK; they deliver broken content types
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
* @return object JSON Schema after walking down the fragment pieces
*
* @throws ResourceNotFoundException
*/
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;
}
/**
* Retrieve a URI
*
* @param string $uri JSON Schema URI
* @param string|null $baseUri
* @return object JSON Schema contents
*/
public function retrieve($uri, $baseUri = null)
{
$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);
}
$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 = array();
if (5 < count($match)) {
$components = array(
'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 boolean
*/
public function isValid($uri)
{
$components = $this->parse($uri);
return !empty($components);
}
}

View file

@ -0,0 +1,40 @@
<?php
/*
* 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\SchemaConstraint;
use JsonSchema\Constraints\Constraint;
/**
* 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 Constraint
{
const SCHEMA_MEDIA_TYPE = 'application/schema+json';
/**
* 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
*
* {@inheritDoc}
*/
public function check($value, $schema = null, $path = null, $i = null)
{
$validator = $this->getFactory()->createInstanceFor('schema');
$validator->check($value, $schema);
$this->addErrors(array_unique($validator->getErrors(), SORT_REGULAR));
}
}