From 737facf3dcade4fa55a902864daa2b64a570c858 Mon Sep 17 00:00:00 2001 From: Erayd Date: Sat, 30 Sep 2017 16:33:18 +1300 Subject: [PATCH] Add proper recursive handling for $ref resolution base Fixes #447 Note that this patch does not check whether a given container is actually a schema when recursing into it. In most cases this will not matter, however it does mean that in some edge cases it will attempt to resolve a `$ref` in a context where ref is actually not part of the spec. Limiting resolution to schema-context containers is outside the scope of this patch, but can be added later. --- src/JsonSchema/SchemaStorage.php | 41 +++++++++++++++++++++++++----- src/JsonSchema/Uri/UriResolver.php | 11 ++++++++ src/JsonSchema/Validator.php | 9 +++++-- tests/Uri/UriResolverTest.php | 33 ++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/JsonSchema/SchemaStorage.php b/src/JsonSchema/SchemaStorage.php index de2a1cb2..58650cd9 100644 --- a/src/JsonSchema/SchemaStorage.php +++ b/src/JsonSchema/SchemaStorage.php @@ -5,7 +5,6 @@ use JsonSchema\Constraints\BaseConstraint; use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\UnresolvableJsonPointerException; -use JsonSchema\Iterator\ObjectIterator; use JsonSchema\Uri\UriResolver; use JsonSchema\Uri\UriRetriever; @@ -69,14 +68,42 @@ public function addSchema($id, $schema = null) } } - $objectIterator = new ObjectIterator($schema); - foreach ($objectIterator as $toResolveSchema) { - if (property_exists($toResolveSchema, '$ref') && is_string($toResolveSchema->{'$ref'})) { - $jsonPointer = new JsonPointer($this->uriResolver->resolve($toResolveSchema->{'$ref'}, $id)); - $toResolveSchema->{'$ref'} = (string) $jsonPointer; + // resolve references + $this->expandRefs($schema, $id); + + $this->schemas[$id] = $schema; + } + + /** + * Recursively resolve all references against the provided base + * + * @param mixed $schema + * @param string $base + */ + private function expandRefs(&$schema, $base = null) + { + if (!is_object($schema)) { + if (is_array($schema)) { + foreach ($schema as &$member) { + $this->expandRefs($member, $base); + } } + + return; + } + + if (property_exists($schema, 'id') && is_string($schema->id)) { + $base = $this->uriResolver->resolve($schema->id, $base); + } + + if (property_exists($schema, '$ref') && is_string($schema->{'$ref'})) { + $refPointer = new JsonPointer($this->uriResolver->resolve($schema->{'$ref'}, $base)); + $schema->{'$ref'} = (string) $refPointer; + } + + foreach ($schema as &$member) { + $this->expandRefs($member, $base); } - $this->schemas[$id] = $schema; } /** diff --git a/src/JsonSchema/Uri/UriResolver.php b/src/JsonSchema/Uri/UriResolver.php index f26046d0..ced7a8da 100644 --- a/src/JsonSchema/Uri/UriResolver.php +++ b/src/JsonSchema/Uri/UriResolver.php @@ -76,6 +76,17 @@ public function generate(array $components) */ public function resolve($uri, $baseUri = null) { + // treat non-uri base as local file path + if (!is_null($baseUri) && !filter_var($baseUri, \FILTER_VALIDATE_URL)) { + 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; } diff --git a/src/JsonSchema/Validator.php b/src/JsonSchema/Validator.php index 8d1b50c9..5d031881 100644 --- a/src/JsonSchema/Validator.php +++ b/src/JsonSchema/Validator.php @@ -52,12 +52,17 @@ public function validate(&$value, $schema = null, $checkMode = null) } // add provided schema to SchemaStorage with internal URI to allow internal $ref resolution - $this->factory->getSchemaStorage()->addSchema(SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI, $schema); + if (is_object($schema) && property_exists($schema, 'id')) { + $schemaURI = $schema->id; + } else { + $schemaURI = SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI; + } + $this->factory->getSchemaStorage()->addSchema($schemaURI, $schema); $validator = $this->factory->createInstanceFor('schema'); $validator->check( $value, - $this->factory->getSchemaStorage()->getSchema(SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI) + $this->factory->getSchemaStorage()->getSchema($schemaURI) ); $this->factory->setConfig($initialCheckMode); diff --git a/tests/Uri/UriResolverTest.php b/tests/Uri/UriResolverTest.php index 456b13d3..051c2a58 100644 --- a/tests/Uri/UriResolverTest.php +++ b/tests/Uri/UriResolverTest.php @@ -190,4 +190,37 @@ public function testReversable() // check that the recombined URI matches the original input $this->assertEquals($uri, $this->resolver->generate($split)); } + + public function testRelativeFileAsRoot() + { + $this->assertEquals( + 'file://' . getcwd() . '/src/JsonSchema/Validator.php', + $this->resolver->resolve( + 'Validator.php', + 'src/JsonSchema/SchemaStorage.php' + ) + ); + } + + public function testRelativeDirectoryAsRoot() + { + $this->assertEquals( + 'file://' . getcwd() . '/src/JsonSchema/Validator.php', + $this->resolver->resolve( + 'Validator.php', + 'src/JsonSchema' + ) + ); + } + + public function testRelativeNonExistentFileAsRoot() + { + $this->assertEquals( + 'file://' . getcwd() . '/resolved.file', + $this->resolver->resolve( + 'resolved.file', + 'test.file' + ) + ); + } }