diff --git a/composer.json b/composer.json index f1f6faea..3e26b0f4 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "json-schema/JSON-Schema-Test-Suite": "1.2.0", "phpunit/phpunit": "^4.8.22", "friendsofphp/php-cs-fixer": "^2.1", - "phpdocumentor/phpdocumentor": "~2" + "phpdocumentor/phpdocumentor": "^2.7" }, "autoload": { "psr-4": { "JsonSchema\\": "src/JsonSchema/" } diff --git a/src/JsonSchema/Constraints/BaseConstraint.php b/src/JsonSchema/Constraints/BaseConstraint.php index eefb08ed..63968213 100644 --- a/src/JsonSchema/Constraints/BaseConstraint.php +++ b/src/JsonSchema/Constraints/BaseConstraint.php @@ -137,12 +137,12 @@ public static function arrayToObjectRecursive($array) $json = json_encode($array); if (json_last_error() !== \JSON_ERROR_NONE) { $message = 'Unable to encode schema array as JSON'; - if (version_compare(phpversion(), '5.5.0', '>=')) { + if (function_exists('json_last_error_msg')) { $message .= ': ' . json_last_error_msg(); } throw new InvalidArgumentException($message); } - return json_decode($json); + return (object) json_decode($json); } } diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index b7f3bb42..e50c7d0b 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -77,13 +77,15 @@ protected function checkArray(&$value, $schema = null, JsonPointer $path = null, * @param mixed $value * @param mixed $schema * @param JsonPointer|null $path - * @param mixed $i + * @param mixed $properties + * @param mixed $additionalProperties * @param mixed $patternProperties */ - protected function checkObject(&$value, $schema = null, JsonPointer $path = null, $i = null, $patternProperties = null, $appliedDefaults = array()) + protected function checkObject(&$value, $schema = null, JsonPointer $path = null, $properties = null, + $additionalProperties = null, $patternProperties = null, $appliedDefaults = array()) { $validator = $this->factory->createInstanceFor('object'); - $validator->check($value, $schema, $path, $i, $patternProperties, $appliedDefaults); + $validator->check($value, $schema, $path, $properties, $additionalProperties, $patternProperties, $appliedDefaults); $this->addErrors($validator->getErrors()); } diff --git a/src/JsonSchema/Constraints/FormatConstraint.php b/src/JsonSchema/Constraints/FormatConstraint.php index c172847f..578cdb14 100644 --- a/src/JsonSchema/Constraints/FormatConstraint.php +++ b/src/JsonSchema/Constraints/FormatConstraint.php @@ -80,6 +80,13 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i = 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 'uriref': + case 'uri-reference': if (null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) { // FILTER_VALIDATE_URL does not conform to RFC-3986, and cannot handle relative URLs, but // the json-schema spec uses RFC-3986, so need a bit of hackery to properly validate them. diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index 0b9a7da3..0010d294 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -27,7 +27,8 @@ class ObjectConstraint extends Constraint /** * {@inheritdoc} */ - public function check(&$element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null, $appliedDefaults = array()) + public function check(&$element, $schema = null, JsonPointer $path = null, $properties = null, + $additionalProp = null, $patternProperties = null, $appliedDefaults = array()) { if ($element instanceof UndefinedConstraint) { return; @@ -37,16 +38,17 @@ public function check(&$element, $definition = null, JsonPointer $path = null, $ $matches = array(); if ($patternProperties) { + // validate the element pattern properties $matches = $this->validatePatternProperties($element, $path, $patternProperties); } - if ($definition) { - // validate the definition properties - $this->validateDefinition($element, $definition, $path); + if ($properties) { + // validate the element properties + $this->validateProperties($element, $properties, $path); } - // additional the element properties - $this->validateElement($element, $matches, $definition, $path, $additionalProp); + // validate additional element properties & constraints + $this->validateElement($element, $matches, $schema, $path, $properties, $additionalProp); } public function validatePatternProperties($element, JsonPointer $path = null, $patternProperties) @@ -81,18 +83,20 @@ public function validatePatternProperties($element, JsonPointer $path = null, $p /** * Validates the element properties * - * @param \stdClass $element Element to validate - * @param array $matches Matches from patternProperties (if any) - * @param \stdClass $objectDefinition ObjectConstraint definition - * @param JsonPointer|null $path Path to test? - * @param mixed $additionalProp Additional 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, $objectDefinition = null, JsonPointer $path = null, $additionalProp = null) + public function validateElement($element, $matches, $schema = null, JsonPointer $path = null, + $properties = null, $additionalProp = null) { - $this->validateMinMaxConstraint($element, $objectDefinition, $path); + $this->validateMinMaxConstraint($element, $schema, $path); foreach ($element as $i => $value) { - $definition = $this->getProperty($objectDefinition, $i); + $definition = $this->getProperty($properties, $i); // no additional properties allowed if (!in_array($i, $matches) && $additionalProp === false && $this->inlineSchemaProperty !== $i && !$definition) { @@ -124,17 +128,17 @@ public function validateElement($element, $matches, $objectDefinition = null, Js /** * Validates the definition properties * - * @param \stdClass $element Element to validate - * @param \stdClass $objectDefinition ObjectConstraint definition - * @param JsonPointer|null $path Path? + * @param \stdClass $element Element to validate + * @param \stdClass $properties Property definitions + * @param JsonPointer|null $path Path? */ - public function validateDefinition(&$element, $objectDefinition = null, JsonPointer $path = null) + public function validateProperties(&$element, $properties = null, JsonPointer $path = null) { $undefinedConstraint = $this->factory->createInstanceFor('undefined'); - foreach ($objectDefinition as $i => $value) { + foreach ($properties as $i => $value) { $property = &$this->getProperty($element, $i, $undefinedConstraint); - $definition = $this->getProperty($objectDefinition, $i); + $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? diff --git a/src/JsonSchema/Constraints/SchemaConstraint.php b/src/JsonSchema/Constraints/SchemaConstraint.php index 6b4fefde..eed53956 100644 --- a/src/JsonSchema/Constraints/SchemaConstraint.php +++ b/src/JsonSchema/Constraints/SchemaConstraint.php @@ -35,16 +35,17 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i = // passed schema $validationSchema = $schema; } elseif ($this->getTypeCheck()->propertyExists($element, $this->inlineSchemaProperty)) { - $inlineSchema = $this->getTypeCheck()->propertyGet($element, $this->inlineSchemaProperty); - if (is_array($inlineSchema)) { - $inlineSchema = json_decode(json_encode($inlineSchema)); - } // inline schema - $validationSchema = $inlineSchema; + $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)) { diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index ec43467e..a86afcac 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -72,8 +72,9 @@ public function validateTypes(&$value, $schema = null, JsonPointer $path, $i = n // is not set (i.e. don't use $this->getTypeCheck() here). $this->checkObject( $value, - isset($schema->properties) ? $this->factory->getSchemaStorage()->resolveRefSchema($schema->properties) : $schema, + $schema, $path, + isset($schema->properties) ? $schema->properties : null, isset($schema->additionalProperties) ? $schema->additionalProperties : null, isset($schema->patternProperties) ? $schema->patternProperties : null, $this->appliedDefaults @@ -251,8 +252,14 @@ protected function applyDefaultValues(&$value, $schema, $path) } } } elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) { + $items = array(); + 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 ($schema->items as $currentItem => $itemDefinition) { + foreach ($items as $currentItem => $itemDefinition) { if ( !array_key_exists($currentItem, $value) && property_exists($itemDefinition, 'default') diff --git a/src/JsonSchema/Rfc3339.php b/src/JsonSchema/Rfc3339.php index fb2eb7d5..adca581a 100644 --- a/src/JsonSchema/Rfc3339.php +++ b/src/JsonSchema/Rfc3339.php @@ -4,7 +4,7 @@ class Rfc3339 { - const REGEX = '/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d+)?(Z|([+-]\d{2}):?(\d{2}))$/'; + 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 @@ -22,8 +22,8 @@ public static function createFromString($string) $dateAndTime = $matches[1]; $microseconds = $matches[2] ?: '.000000'; $timeZone = 'Z' !== $matches[3] ? $matches[4] . ':' . $matches[5] : '+00:00'; - - $dateTime = \DateTime::createFromFormat('Y-m-d\TH:i:s.uP', $dateAndTime . $microseconds . $timeZone, new \DateTimeZone('UTC')); + $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; } diff --git a/src/JsonSchema/SchemaStorage.php b/src/JsonSchema/SchemaStorage.php index 3ee081e3..9a1caeb4 100644 --- a/src/JsonSchema/SchemaStorage.php +++ b/src/JsonSchema/SchemaStorage.php @@ -2,6 +2,7 @@ namespace JsonSchema; +use JsonSchema\Constraints\BaseConstraint; use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\UnresolvableJsonPointerException; use JsonSchema\Iterator\ObjectIterator; @@ -51,6 +52,23 @@ public function addSchema($id, $schema = null) // 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'; + } + } + $objectIterator = new ObjectIterator($schema); foreach ($objectIterator as $toResolveSchema) { if (property_exists($toResolveSchema, '$ref') && is_string($toResolveSchema->{'$ref'})) { diff --git a/src/JsonSchema/Validator.php b/src/JsonSchema/Validator.php index 9554175f..7b6a8077 100644 --- a/src/JsonSchema/Validator.php +++ b/src/JsonSchema/Validator.php @@ -57,7 +57,10 @@ public function validate(&$value, $schema = null, $checkMode = null) $this->factory->getSchemaStorage()->addSchema(SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI, $schema); $validator = $this->factory->createInstanceFor('schema'); - $validator->check($value, $schema); + $validator->check( + $value, + $this->factory->getSchemaStorage()->getSchema(SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI) + ); $this->factory->setConfig($initialCheckMode); diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index a3b9c4e2..bcf90b2c 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -149,6 +149,21 @@ public function getValidTests() '{"items":[{"default":null}]}', '[null]' ), + array(// #21 items might be a schema (instead of an array of schema) + '[{}]', + '{"items":{"properties":{"propertyOne":{"default":"valueOne"}}}}', + '[{"propertyOne":"valueOne"}]' + ), + array(// #22 if items is not an array, it does not create a new item + '[]', + '{"items":{"properties":{"propertyOne":{"default":"valueOne"}}}}', + '[]' + ), + array(// #23 if items is a schema with a default value and minItems is present, fill the array + '["a"]', + '{"items":{"default":"b"}, "minItems": 3}', + '["a","b","b"]' + ), ); } diff --git a/tests/Constraints/FormatTest.php b/tests/Constraints/FormatTest.php index ad7075d9..b035aafe 100644 --- a/tests/Constraints/FormatTest.php +++ b/tests/Constraints/FormatTest.php @@ -143,12 +143,12 @@ public function getValidFormats() array('555 320 1212', 'phone'), array('http://bluebox.org', 'uri'), - array('//bluebox.org', 'uri'), - array('/absolutePathReference/', 'uri'), - array('./relativePathReference/', 'uri'), - array('./relative:PathReference/', 'uri'), - array('relativePathReference/', 'uri'), - array('relative/Path:Reference/', 'uri'), + array('//bluebox.org', 'uri-reference'), + array('/absolutePathReference/', 'uri-reference'), + array('./relativePathReference/', 'uri-reference'), + array('./relative:PathReference/', 'uri-reference'), + array('relativePathReference/', 'uri-reference'), + array('relative/Path:Reference/', 'uri-reference'), array('info@something.edu', 'email'), @@ -200,6 +200,12 @@ public function getInvalidFormats() array('htt:/bluebox.org', 'uri'), array('.relative:path/reference/', 'uri'), array('', 'uri'), + array('//bluebox.org', 'uri'), + array('/absolutePathReference/', 'uri'), + array('./relativePathReference/', 'uri'), + array('./relative:PathReference/', 'uri'), + array('relativePathReference/', 'uri'), + array('relative/Path:Reference/', 'uri'), array('info@somewhere', 'email'), diff --git a/tests/Constraints/MinMaxPropertiesTest.php b/tests/Constraints/MinMaxPropertiesTest.php index e4fd5a1a..2063122c 100644 --- a/tests/Constraints/MinMaxPropertiesTest.php +++ b/tests/Constraints/MinMaxPropertiesTest.php @@ -74,7 +74,7 @@ public function getInvalidTests() return array( array( '{ - "value": 1 + "value": {} }', '{ "type": "object", @@ -83,9 +83,27 @@ public function getInvalidTests() } }' ), + array( + '{}', + '{ + "type": "object", + "properties": { + "propertyOne": { + "type": "string" + }, + "propertyTwo": { + "type": "string" + } + }, + "minProperties": 1 + }' + ), array( '{ - "value": 1 + "value": { + "propertyOne": "valueOne", + "propertyTwo": "valueTwo" + } }', '{ "type": "object", diff --git a/tests/Rfc3339Test.php b/tests/Rfc3339Test.php index d01da520..13294d0a 100644 --- a/tests/Rfc3339Test.php +++ b/tests/Rfc3339Test.php @@ -35,8 +35,14 @@ public function provideValidFormats() '2000-05-01T12:12:12Z', \DateTime::createFromFormat('Y-m-d\TH:i:s', '2000-05-01T12:12:12', new \DateTimeZone('UTC')) ), - array('2000-05-01T12:12:12+0100', \DateTime::createFromFormat('Y-m-d\TH:i:sP', '2000-05-01T12:12:12+01:00')), - array('2000-05-01T12:12:12+01:00', \DateTime::createFromFormat('Y-m-d\TH:i:sP', '2000-05-01T12:12:12+01:00')), + array( + '2000-05-01T12:12:12+0100', + \DateTime::createFromFormat('Y-m-d\TH:i:sP', '2000-05-01T12:12:12+01:00') + ), + array( + '2000-05-01T12:12:12+01:00', + \DateTime::createFromFormat('Y-m-d\TH:i:sP', '2000-05-01T12:12:12+01:00') + ), array( '2000-05-01T12:12:12.123456Z', \DateTime::createFromFormat('Y-m-d\TH:i:s.u', '2000-05-01T12:12:12.123456', new \DateTimeZone('UTC')) @@ -45,6 +51,14 @@ public function provideValidFormats() '2000-05-01T12:12:12.123Z', \DateTime::createFromFormat('Y-m-d\TH:i:s.u', '2000-05-01T12:12:12.123000', new \DateTimeZone('UTC')) ), + array( + '2000-05-01 12:12:12.123Z', + \DateTime::createFromFormat('Y-m-d H:i:s.u', '2000-05-01 12:12:12.123000', new \DateTimeZone('UTC')) + ), + array( + '2000-05-01 12:12:12.123456Z', + \DateTime::createFromFormat('Y-m-d H:i:s.u', '2000-05-01 12:12:12.123456', new \DateTimeZone('UTC')) + ) ); } @@ -54,6 +68,8 @@ public function provideInvalidFormats() array('1999-1-11T00:00:00Z'), array('1999-01-11T00:00:00+100'), array('1999-01-11T00:00:00+1:00'), + array('1999-01-01 00:00:00Z'), + array('1999-1-11 00:00:00Z') ); } } diff --git a/tests/SchemaStorageTest.php b/tests/SchemaStorageTest.php index 294f0f95..0e440ac8 100644 --- a/tests/SchemaStorageTest.php +++ b/tests/SchemaStorageTest.php @@ -289,4 +289,17 @@ public function testGetUriResolver() $s->addSchema('http://json-schema.org/draft-04/schema#'); $this->assertInstanceOf('\JsonSchema\Uri\UriResolver', $s->getUriResolver()); } + + public function testMetaSchemaFixes() + { + $s = new SchemaStorage(); + $s->addSchema('http://json-schema.org/draft-03/schema#'); + $s->addSchema('http://json-schema.org/draft-04/schema#'); + $draft_03 = $s->getSchema('http://json-schema.org/draft-03/schema#'); + $draft_04 = $s->getSchema('http://json-schema.org/draft-04/schema#'); + + $this->assertEquals('uri-reference', $draft_03->properties->id->format); + $this->assertEquals('uri-reference', $draft_03->properties->{'$ref'}->format); + $this->assertEquals('uri-reference', $draft_04->properties->id->format); + } }