Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/JsonSchema/Exception/InvalidPointerException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace JsonSchema\Exception;

class InvalidPointerException extends \RuntimeException
{
}
125 changes: 125 additions & 0 deletions src/JsonSchema/PointerResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

namespace JsonSchema;

use JsonSchema\Exception\InvalidPointerException;
use JsonSchema\Exception\ResourceNotFoundException;

/**
* Resolve JSON Pointers (RFC 6901)
*/
class PointerResolver
{
const EMPTY_ELEMENT = '_empty_';
const LAST_ELEMENT = '-';
const SEPARATOR = '/';

/**
* Get the part of the document which the pointer points to.
*
* @param object $json The json document to resolve within.
* @param string $pointer The Json Pointer.
*
* @throws InvalidPointerException
* @throws ResourceNotFoundException
*
* @return mixed
*/
public function resolvePointer($json, $pointer)
{
if ($pointer === '') {
return $json;
}

$this->validatePointer($pointer);

$parts = array_slice(array_map('urldecode', explode('/', $pointer)), 1);

return $this->resolve($json, $pointer, $this->decodeParts($parts));
}

/**
* Decode any escaped sequences.
*
* @param array $parts The json pointer parts.
*
* @return array
*/
private function decodeParts(array $parts)
{
$mappings = array(
'~1' => '/',
'~0' => '~',
);

foreach ($parts as &$part) {
$part = strtr($part, $mappings);
}

return $parts;
}

/**
* Recurse through $json until location described by $parts is found.
*
* @param mixed $json The json document.
* @param string $json The original json pointer.
* @param array $parts The (remaining) parts of the pointer.
*
* @throws ResourceNotFoundException
*
* @return mixed
*/
private function resolve($json, $pointer, array $parts)
{
// Check for completion
if (count($parts) === 0) {
return $json;
}

$part = array_shift($parts);

// Ensure we deal with empty keys the same way as json_decode does
if ($part === '') {
$part = self::EMPTY_ELEMENT;
}

if (is_object($json) && property_exists($json, $part)) {
return $this->resolve($json->$part, $pointer, $parts);
} elseif (is_array($json)) {
if ($part === self::LAST_ELEMENT) {
return $this->resolve(end($json), $pointer, $parts);
}
if (filter_var($part, FILTER_VALIDATE_INT) !== false &&
array_key_exists($part, $json)
) {
return $this->resolve($json[$part], $pointer, $parts);
}
}

$message = "Failed to resolve pointer $pointer from document id"
. (isset($json->id) ? $json->id : '');
throw new ResourceNotFoundException($message);
}

/**
* Validate a pointer string.
*
* @param string $pointer The pointer to validate.
*
* @throws InvalidPointerException
*/
private function validatePointer($pointer)
{
if ($pointer !== '' && !is_string($pointer)) {
throw new InvalidPointerException('Pointer is not a string');
}

$firstCharacter = substr($pointer, 0, 1);

if ($firstCharacter !== self::SEPARATOR) {
throw new InvalidPointerException('Pointer starts with invalid character');
}
}

}
158 changes: 94 additions & 64 deletions src/JsonSchema/RefResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use JsonSchema\Exception\JsonDecodingException;
use JsonSchema\Uri\Retrievers\UriRetrieverInterface;
use JsonSchema\Uri\UriRetriever;
use JsonSchema\Uri\UriResolver;

/**
* Take in an object that's a JSON schema and take care of all $ref references
Expand All @@ -21,30 +22,22 @@
*/
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;
const SELF_REF_LOCATION = '#';

/**
* maximum references depth
* @var integer
* @var UriRetrieverInterface
*/
public static $maxDepth = 7;
protected $uriRetriever = null;

/**
* @var UriRetrieverInterface
* @var array
*/
protected $uriRetriever = null;
protected $schemas = array();

/**
* @var object
* @var array
*/
protected $rootSchema = null;
protected $scopes = array();

/**
* @param UriRetriever $retriever
Expand All @@ -63,11 +56,36 @@ public function __construct($retriever = null)
*/
public function fetchRef($ref, $sourceUri)
{
$retriever = $this->getUriRetriever();
$jsonSchema = $retriever->retrieve($ref, $sourceUri);
$this->resolve($jsonSchema);
// Get absolute uri
$resolver = new UriResolver();
$uri = $resolver->resolve($ref, $sourceUri);

// Split in to location and fragment
$location = $resolver->extractLocation($uri);
$fragment = $resolver->extractFragment($uri);

// Retrieve dereferenced schema
if ($location == null) {
$schema = $this->schemas[self::SELF_REF_LOCATION];
} elseif (array_key_exists($location, $this->schemas)) {
$schema = $this->schemas[$location];
} else {
$retriever = $this->getUriRetriever();
$schema = $retriever->retrieve($location);

$this->schemas[$location] = $schema;
$this->resolve($schema, $location);
}

// Resolve JSON pointer
$retriever = $this->getUriRetriever();
$object = $retriever->resolvePointer($schema, $fragment);

if ($object instanceof \stdClass) {
$object->id = $uri;
}

return $jsonSchema;
return $object;
}

/**
Expand Down Expand Up @@ -101,47 +119,75 @@ public function getUriRetriever()
*/
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;
if (!is_object($schema)) {
return;
}

if (null === $sourceUri && ! empty($schema->id)) {
$sourceUri = $schema->id;
// Fill in id property
if ($sourceUri) {
$schema->id = $sourceUri;
}

if (null === $this->rootSchema) {
$this->rootSchema = $schema;
}

// Resolve $ref first
$this->resolveRef($schema, $sourceUri);
// First determine our resolution scope
$scope = $this->enterResolutionScope($schema, $sourceUri);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 :)


// 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);
$this->resolveProperty($schema, $propertyName, $scope);
}

// 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);
$this->resolveArrayOfSchemas($schema, $propertyName, $scope);
}

// These are all objects containing properties whose values are schemas
foreach (array('dependencies', 'patternProperties', 'properties') as $propertyName) {
$this->resolveObjectOfSchemas($schema, $propertyName, $sourceUri);
foreach (array('definitions', 'dependencies', 'patternProperties', 'properties') as $propertyName) {
$this->resolveObjectOfSchemas($schema, $propertyName, $scope);
}

--self::$depth;
// Resolve $ref
$this->resolveRef($schema, $scope);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you should resolveRef before other resolutions. Before 134 line.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for doing it afterward is to ensure we don't get stuck in an infinite loop whilst attempting to dereference everything.


// Pop back out of our scope
$this->leaveResolutionScope();
}

/**
* Enters a new resolution scope for the given schema. Inspects the
* partial for the presence of 'id' and then returns that as a absolute
* uri. Returns the new scope.
*
* @param object $schemaPartial JSON Schema to get the resolution scope for
* @param string $sourceUri URI where this schema was located
* @return string
*/
private function enterResolutionScope($schemaPartial, $sourceUri)
{
if (count($this->scopes) === 0) {
$this->scopes[] = self::SELF_REF_LOCATION;
$this->schemas[self::SELF_REF_LOCATION] = $schemaPartial;
}

if (!empty($schemaPartial->id)) {
$resolver = new UriResolver();
$this->scopes[] = $resolver->resolve($schemaPartial->id, $sourceUri);
} else {
$this->scopes[] = end($this->scopes);
}

return end($this->scopes);
}

/**
* Leaves the current resolution scope.
*/
private function leaveResolutionScope()
{
array_pop($this->scopes);
}

/**
Expand Down Expand Up @@ -215,36 +261,20 @@ public function resolveRef($schema, $sourceUri)
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);
}
// Retrieve the referenced schema
$uri = $schema->$ref;
$refSchema = $this->fetchRef($schema->$ref, $sourceUri, $schema);

// Remove the reference node
unset($schema->$ref);

// Augment the current $schema object with properties fetched
// Augment the properties (FIXME this is a bit naive and might need fixing)
foreach (get_object_vars($refSchema) as $prop => $value) {
$schema->$prop = $value;
}

// Check for nested references
$this->resolveRef($schema, $sourceUri);
}

/**
Expand Down
Loading