Seditio Source
Root |
./othercms/croogo-4.0.7/vendor/friendsofcake/crud-json-api/src/Listener/JsonApi/DocumentValidator.php
<?php
namespace CrudJsonApi\Listener\JsonApi;

use
Cake\ORM\Entity;
use
Cake\Utility\Hash;
use
Cake\Utility\Inflector;
use
Cake\Validation\Validation;
use
Crud\Core\BaseObject;
use
Crud\Error\Exception\CrudException;
use
Crud\Error\Exception\ValidationException;
use
Neomerx\JsonApi\Schema\Error;
use
Neomerx\JsonApi\Schema\ErrorCollection;
use
Neomerx\JsonApi\Schema\Link;
use
stdClass;

/**
 * Validates incoming JSON API documents against the specifications for
 * CRUD actions described at http://jsonapi.org/format/#crud.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 */
class DocumentValidator extends stdClass
{
   
/**
     * RequestHandler decoded JSON API document array.
     *
     * @var array $_document
     */
   
protected $_document;

   
/**
     * @var \Neomerx\JsonApi\Schema\ErrorCollection
     */
   
protected $_errorCollection;

   
/**
     * var array JsonApiListener config() options
     */
   
protected $_config;

   
/**
     * Constructor
     *
     * @param array $documentArray Decoded JSON API document
     * @param array $listenerConfig JsonApiListener config() options
     * @return void
     */
   
public function __construct(array $documentArray, array $listenerConfig)
    {
       
$this->_document = $documentArray;

       
$this->_config = $listenerConfig;

       
$this->_errorCollection = new ErrorCollection();
    }

   
/**
     * Validates a JSON API request data document used for creating
     * resources against the specification requirements described
     * at http://jsonapi.org/format/#crud-creating.
     *
     * @throws \Crud\Error\Exception\ValidationException
     * @return void
     */
   
public function validateCreateDocument()
    {
       
$this->_documentMustHavePrimaryData();
       
$this->_primaryDataMustHaveType();
       
$this->_primaryDataMayHaveUuid();
       
$this->_primaryDataMayHaveRelationships();

        if (
$this->_errorCollection->count() === 0) {
            return;
        }

        throw new
ValidationException($this->_getErrorCollectionEntity());
    }

   
/**
     * Validates a JSON API request data document used for updating
     * resources against the specification requirements described
     * at http://jsonapi.org/format/#crud-updating.
     *
     * @throws \Crud\Error\Exception\ValidationException
     * @return void
     */
   
public function validateUpdateDocument()
    {
       
$this->_documentMustHavePrimaryData();
       
$this->_primaryDataMustHaveType();
       
$this->_primaryDataMustHaveId();
       
$this->_primaryDataMayHaveRelationships();

        if (
$this->_errorCollection->count() === 0) {
            return;
        }

        throw new
ValidationException($this->_getErrorCollectionEntity());
    }

   
/**
     * Document MUST have the top-level member `data`. If not, throw the
     * correct custom validation error with a pointer to '' as described at
     * http://jsonapi.org/examples/#error-objects-source-usage.
     *
     * @throws \Crud\Error\Exception\ValidationException
     * @return bool
     */
   
protected function _documentMustHavePrimaryData()
    {
        if (
$this->_hasProperty('data')) {
            return
true;
        }

       
$this->_errorCollection->add(new Error(
           
$idx = null,
           
$aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#document-top-level'),
           
$typeLinks = null,
           
$status = null,
           
$code = null,
           
$title = null,
           
$detail = "Document does not contain top-level member 'data'",
           
$source = [
               
'pointer' => ''
           
]
        ));

        throw new
ValidationException($this->_getErrorCollectionEntity());
    }

   
/**
     * Ensures primary data has member 'type' which MUST be a string.
     *
     * @return bool
     */
   
protected function _primaryDataMustHaveType()
    {
       
$path = $this->_getPathObject('data.type');

        if (!
$this->_hasProperty($path)) {
           
$this->_errorCollection->addDataError(
               
$title = '_required',
               
$detail = "Primary data does not contain member 'type'",
               
$status = null,
               
$idx = null,
               
$aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating')
            );

            return
false;
        }

       
$value = $this->_getProperty($path->dotted);

        if (
is_string($value)) {
            return
true;
        }

       
$this->_errorCollection->addDataTypeError(
           
$title = '_notString',
           
$details = "Primary data member 'type' is not a string",
           
$status = null,
           
$idx = null,
           
$aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#document-resource-object-identification')
        );

        return
false;
    }

   
/**
     * Ensures primary data has member 'id' which MUST be a string.
     *
     * @return bool
     */
   
protected function _primaryDataMustHaveId()
    {
       
$path = $this->_getPathObject('data.id');

        if (!
$this->_hasProperty($path)) {
           
$this->_errorCollection->addDataError(
               
$title = '_required',
               
$detail = "Primary data does not contain member 'id'",
               
$status = null,
               
$idx = null,
               
$aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-updating')
            );

            return
false;
        }

       
$value = $this->_getProperty($path->dotted);

        if (
is_string($value)) {
            return
true;
        }

       
$this->_errorCollection->addDataIdError(
           
$title = '_notString',
           
$details = "Primary data member 'id' is not a string",
           
$status = null,
           
$idx = null,
           
$aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#document-resource-object-identification')
        );

        return
false;
    }

   
/**
     * Ensures that primary data 'id' member is valid IF it exists.
     *
     * @return bool
     */
   
protected function _primaryDataMayHaveUuid()
    {
       
$path = $this->_getPathObject('data.id');

        if (!
$this->_hasProperty($path)) {
            return
true;
        }

       
$id = $this->_getProperty($path->dotted);

        if (
Validation::uuid($id)) {
            return
true;
        }

       
$this->_errorCollection->addDataIdError(
           
$title = '_notUuid',
           
$details = "Primary data member 'id' is not a valid UUID",
           
$status = null,
           
$idx = null,
           
$aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating-client-ids')
        );

        return
false;
    }

   
/**
     * Ensures that primary data 'relationships' member contains valid data
     * if it exists.
     *
     * @return bool
     */
   
protected function _primaryDataMayHaveRelationships()
    {
       
$path = $this->_getPathObject('data.relationships');

        if (!
$this->_hasProperty($path)) {
            return
true;
        }

       
$relationships = $this->_getProperty($path->dotted);

        if (empty(
$relationships)) {
           
$this->_errorCollection->addRelationshipsError(
               
$title = '_required',
               
$detail = "Relationships object does not contain any members",
               
$status = null,
               
$idx = null,
               
$aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating')
            );

            return
false;
        }

        foreach (
$relationships as $relationship => $data) {
           
$relationshipPathObject = $this->_getPathObject($path->dotted . '.' . $relationship);

            if (!
$this->_relationshipMustHaveData($relationshipPathObject)) {
                continue;
            }

            if (
$this->_relationshipDataIsNull($relationshipPathObject)) {
                continue;
            }

           
// single belongsTo relationship
           
if ($this->_stringIsSingular($relationshipPathObject->key)) {
               
$this->_relationshipDataMustHaveType($relationship, $relationshipPathObject);
               
$this->_relationshipDataMustHaveId($relationship, $relationshipPathObject);

                continue;
            }

           
// multiple hasMany relationships
           
$hasManys = $this->_getProperty($relationshipPathObject->dotted . '.data');

           
$i = 0;
            foreach (
$hasManys as $hasMany) {
               
$pathObject = $this->_getPathObject($relationshipPathObject->dotted . '.data.' . $i);

               
$this->_relationshipDataMustHaveType($relationship, $pathObject);
               
$this->_relationshipDataMustHaveId($relationship, $pathObject);

               
$i++;
            }
        }

        return
true;
    }

   
/**
     * Ensures a relationship object has a 'data' member.
     *
     * @param string|stdClass $path Dot separated path of relationship object or path object
     * @return bool
     */
   
protected function _relationshipMustHaveData($path)
    {
       
$path = $this->_getPathObject($path);

        if (
$this->_hasProperty($path->dotted . '.data')) {
            return
true;
        }

       
$this->_errorCollection->addRelationshipError(
           
$name = $path->key,
           
$title = '_required',
           
$detail = "Relationships object does not contain member 'data'",
           
$status = null,
           
$idx = null,
           
$aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating')
        );

        return
false;
    }

   
/**
     * Checks if relationship object has 'data' member set to null which is
     * allowed by the JSON API spec.
     *
     * @param string|stdClass $path Dot separated path of relationship object or path object
     * @return bool
     */
   
protected function _relationshipDataIsNull($path)
    {
       
$path = $this->_getPathObject($path);

        if (
$this->_getProperty($path->dotted . '.data') === null) {
            return
true;
        }

        return
false;
    }

   
/**
     * Ensures a relationship data has a 'type' member.
     *
     * @param string $relationship Singular or plural relationship name
     * @param string|stdClass $path Dot separated path of relationship object or path object
     * @return bool
     */
   
protected function _relationshipDataMustHaveType($relationship, $path)
    {
       
$path = $this->_getPathObject($path);

       
// generate correct feedback and path for hasMany and belongsTo relationships
       
$array = $this->_getProperty($path);
       
$arrayDepth = Hash::dimensions($array);

        if (
$arrayDepth === 1) {
           
$searchPath = $path->dotted . '.type'; // hasMany
           
$pointer = $relationship . '/data/' . $path->key;
        } else {
           
$searchPath = $path->dotted . '.data.type'; // belongsTo
           
$pointer = $relationship . '/data';
        }

       
// make sure the relationship data has the `type` key
       
if (!$this->_hasProperty($searchPath)) {
           
$this->_errorCollection->addRelationshipError(
               
$name = $pointer,
               
$title = '_required',
               
$detail = "Relationship data does not contain member 'type'",
               
$status = null,
               
$idx = null,
               
$aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating')
            );

            return
false;
        }

       
// key exists so update the pointer before checking if value is a string
       
$pointer = $pointer . '/type';

        if (!
$this->_isString($searchPath)) {
           
$this->_errorCollection->addRelationshipError(
               
$name = $pointer,
               
$title = '_required',
               
$detail = "Relationship data member 'type' is not a string",
               
$status = null,
               
$idx = null,
               
$aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating')
            );

            return
false;
        }

        return
true;
    }

   
/**
     * Ensures relationship data has an 'id' member.
     *
     * @param string $relationship Singular or plural relationship name
     * @param string|stdClass $path Dot separated path of relationship object or path object
     * @return bool
     */
   
protected function _relationshipDataMustHaveId($relationship, $path)
    {
       
$path = $this->_getPathObject($path);

       
// generate correct feedback and path for hasMany and belongsTo relationships
       
$array = $this->_getProperty($path);
       
$arrayDepth = Hash::dimensions($array);

        if (
$arrayDepth === 1) {
           
$searchPath = $path->dotted . '.id'; // hasMany
           
$pointer = $relationship . '/data/' . $path->key;
        } else {
           
$searchPath = $path->dotted . '.data.id'; // belongsTo
           
$pointer = $relationship . '/data';
        }

       
// make sure the relationship data has the `type` key
       
if (!$this->_hasProperty($searchPath)) {
           
$this->_errorCollection->addRelationshipError(
               
$name = $pointer,
               
$title = '_required',
               
$detail = "Relationship data does not contain member 'id'",
               
$status = null,
               
$idx = null,
               
$aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating')
            );

            return
false;
        }

       
// key exists so update the pointer before checking if value is a string
       
$pointer = $pointer . '/id';

        if (!
$this->_isString($searchPath)) {
           
$this->_errorCollection->addRelationshipError(
               
$name = $pointer,
               
$title = '_required',
               
$detail = "Relationship data member 'type' is not a string",
               
$status = null,
               
$idx = null,
               
$aboutLink = $this->_getAboutLink('http://jsonapi.org/format/#crud-creating')
            );

            return
false;
        }

        return
true;
    }

   
/**
     * Checks if a document property is a string.
     *
     * @param string $path Dot separated path of the property
     * @return bool
     */
   
protected function _isString($path)
    {
       
$path = $this->_getPathObject($path);

        if (!
$this->_hasProperty($path)) {
            throw new
CrudException("Document member '$path->dotted' does not exist");
        }

       
$value = $this->_getProperty($path->dotted);

        if (
is_string($value)) {
            return
true;
        }

        return
false;
    }

   
/**
     * Checks if a document property is a valid UUID.
     *
     * @throws \Crud\Error\Exception\CrudException
     * @param string $path Dot separated path of the property
     * @return bool
     */
   
protected function _isUuid($path)
    {
       
$path = $this->_getPathObject($path);

        if (!
$this->_hasProperty($path)) {
            throw new
CrudException("Document member '$path->dotted' does not exist");
        }

       
$id = $this->_getProperty($path->dotted);

        if (
Validation::uuid($id)) {
            return
true;
        }

        return
false;
    }

   
/**
     * Checks if document contains a given property (even when value
     * is `false` or `null`).
     *
     * @param string|stdClass $path Dot separated path of the property or a path object
     * @return mixed|bool
     */
   
protected function _hasProperty($path)
    {
        if (
is_a($path, 'stdClass')) {
           
$path = $path->dotted;
        }

       
$current = $this->_document;
       
$parts = strtok($path, '.');

        while (
$parts !== false) {
            if (!
array_key_exists($parts, $current)) {
                return
false;
            }
           
$current = $current[$parts];
           
$parts = strtok('.');
        }

        return
true;
    }

   
/**
     * Returns the value for a given document property.
     *
     * @param string|stdClass $path Dot separated path of the property or path object
     * @throws \Crud\Error\Exception\CrudException
     * @return mixed
     */
   
protected function _getProperty($path)
    {
        if (
is_a($path, 'stdClass')) {
           
$path = $path->dotted;
        }

       
$current = $this->_document;

       
$pathClone = $path;
       
$parts = strtok($pathClone, '.');

        while (
$parts !== false) {
            if (!
array_key_exists($parts, $current)) {
                throw new
CrudException("Error retrieving a value for non-existing JSON API document property '$path'");
            }
           
$current = $current[$parts];
           
$parts = strtok('.');
        }

        return
$current;
    }

   
/**
     * Helper method to create an object with consistent path strings from
     * given dot separated path.
     *
     * @param string|stdClass $path Dot separated path or stdClass $path object
     * @return \stdClass
     */
   
protected function _getPathObject($path)
    {
       
// return as-is if parameter is
       
if (is_a($path, 'stdClass')) {
            return
$path;
        }

       
// create path object from given string
       
$obj = new stdClass();
       
$obj->dotted = $path;

       
$parts = explode('.', $path);

        if (
count($parts) === 1) {
           
$obj->toKey = null;
           
$obj->key = $path;

            return
$obj;
        }

       
$key = end($parts);
       
array_pop($parts);
       
$obj->toKey = implode('.', $parts);
       
$obj->key = $key;

        return
$obj;
    }

   
/**
     * Helper method that displays aboutLink only if enabled in Listener config.
     *
     * @param string $url URL
     * @return \Neomerx\JsonApi\Schema\Link|null
     */
   
protected function _getAboutLink($url)
    {
        if (
$this->_config['docValidatorAboutLinks'] === false) {
            return
null;
        }

        return new
Link(false, $url, false);
    }

   
/**
     * Helper method to make the ErrorCollection object available inside the
     * JsonApiExceptionRenderer validation() method by cloaking it as a
     * default CakePHP validation error.
     *
     * @throws \Crud\Error\Exception\ValidationException
     * @return \Cake\ORM\Entity
     */
   
protected function _getErrorCollectionEntity()
    {
       
$entity = new Entity();

       
$entity->setErrors([
           
'CrudJsonApiListener' => [
               
'NeoMerxErrorCollection' => $this->_errorCollection
           
]
        ]);

        return
$entity;
    }

   
/**
     * Helper function to determine if string is singular or plural.
     *
     * @param string $string Preferably a CakePHP generated name.
     * @return bool
     */
   
protected function _stringIsSingular($string)
    {
        if (
Inflector::singularize($string) === $string) {
            return
true;
        }

        return
false;
    }
}