You are here

class ResourceResponseValidator in JSON:API 8

Same name and namespace in other branches
  1. 8.2 src/EventSubscriber/ResourceResponseValidator.php \Drupal\jsonapi\EventSubscriber\ResourceResponseValidator

Response subscriber that validates a JSON API response.

This must run after ResourceResponseSubscriber.

@internal

Hierarchy

  • class \Drupal\jsonapi\EventSubscriber\ResourceResponseValidator implements \Symfony\Component\EventDispatcher\EventSubscriberInterface

Expanded class hierarchy of ResourceResponseValidator

See also

\Drupal\rest\EventSubscriber\ResourceResponseSubscriber

1 file declares its use of ResourceResponseValidator
ResourceResponseValidatorTest.php in tests/src/Unit/EventSubscriber/ResourceResponseValidatorTest.php
1 string reference to 'ResourceResponseValidator'
jsonapi.services.yml in ./jsonapi.services.yml
jsonapi.services.yml
1 service uses ResourceResponseValidator
jsonapi.resource_response_validator.subscriber in ./jsonapi.services.yml
Drupal\jsonapi\EventSubscriber\ResourceResponseValidator

File

src/EventSubscriber/ResourceResponseValidator.php, line 28

Namespace

Drupal\jsonapi\EventSubscriber
View source
class ResourceResponseValidator implements EventSubscriberInterface {

  /**
   * The serializer.
   *
   * @var \Symfony\Component\Serializer\SerializerInterface
   */
  protected $serializer;

  /**
   * The JSON API logger channel.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

  /**
   * The schema validator.
   *
   * This property will only be set if the validator library is available.
   *
   * @var \JsonSchema\Validator|null
   */
  protected $validator;

  /**
   * The schemata schema factory.
   *
   * This property will only be set if the schemata module is installed.
   *
   * @var \Drupal\schemata\SchemaFactory|null
   */
  protected $schemaFactory;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The application's root file path.
   *
   * @var string
   */
  protected $appRoot;

  /**
   * Constructs a ResourceResponseValidator object.
   *
   * @param \Symfony\Component\Serializer\SerializerInterface $serializer
   *   The serializer.
   * @param \Psr\Log\LoggerInterface $logger
   *   The JSON API logger channel.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param string $app_root
   *   The application's root file path.
   */
  public function __construct(SerializerInterface $serializer, LoggerInterface $logger, ModuleHandlerInterface $module_handler, $app_root) {
    $this->serializer = $serializer;
    $this->logger = $logger;
    $this->moduleHandler = $module_handler;
    $this->appRoot = $app_root;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events[KernelEvents::RESPONSE][] = [
      'onResponse',
    ];
    return $events;
  }

  /**
   * Sets the validator service if available.
   */
  public function setValidator(Validator $validator = NULL) {
    if ($validator) {
      $this->validator = $validator;
    }
    elseif (class_exists(Validator::class)) {
      $this->validator = new Validator();
    }
  }

  /**
   * Injects the schema factory.
   *
   * @param \Drupal\schemata\SchemaFactory $schema_factory
   *   The schema factory service.
   */
  public function setSchemaFactory(SchemaFactory $schema_factory) {
    $this->schemaFactory = $schema_factory;
  }

  /**
   * Validates JSON API responses.
   *
   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
   *   The event to process.
   */
  public function onResponse(FilterResponseEvent $event) {
    $response = $event
      ->getResponse();
    if (!$response instanceof ResourceResponse) {
      return;
    }
    $this
      ->doValidateResponse($response, $event
      ->getRequest());
  }

  /**
   * Wraps validation in an assert to prevent execution in production.
   *
   * @see self::validateResponse
   */
  public function doValidateResponse(Response $response, Request $request) {
    if (PHP_MAJOR_VERSION >= 7 || assert_options(ASSERT_ACTIVE)) {
      assert($this
        ->validateResponse($response, $request), 'A JSON API response failed validation (see the logs for details). Please report this in the issue queue on drupal.org');
    }
  }

  /**
   * Validates a response against the JSON API specification.
   *
   * @param \Symfony\Component\HttpFoundation\Response $response
   *   The response to validate.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request containing info about what to validate.
   *
   * @return bool
   *   FALSE if the response failed validation, otherwise TRUE.
   */
  protected function validateResponse(Response $response, Request $request) {

    // If the validator isn't set, then the validation library is not installed.
    if (!$this->validator) {
      return TRUE;
    }

    // Do not use Json::decode here since it coerces the response into an
    // associative array, which creates validation errors.
    $response_data = json_decode($response
      ->getContent());
    if (empty($response_data)) {
      return TRUE;
    }
    $schema_ref = sprintf('file://%s/schema.json', implode('/', [
      $this->appRoot,
      $this->moduleHandler
        ->getModule('jsonapi')
        ->getPath(),
    ]));
    $generic_jsonapi_schema = (object) [
      '$ref' => $schema_ref,
    ];
    $is_valid = $this
      ->validateSchema($generic_jsonapi_schema, $response_data);
    if (!$is_valid) {
      return FALSE;
    }

    // This will be set if the schemata module is present.
    if (!$this->schemaFactory) {

      // Fall back the valid generic result since schemata is absent.
      return TRUE;
    }

    // Get the schema for the current resource. For that we will need to
    // introspect the request to find the entity type and bundle matched by the
    // router.
    $resource_type = $request
      ->get(Routes::RESOURCE_TYPE_KEY);
    $route_name = $request->attributes
      ->get(RouteObjectInterface::ROUTE_NAME);

    // We shouldn't validate related/relationships.
    $is_related = strpos($route_name, '.related') !== FALSE;
    $is_relationship = strpos($route_name, '.relationship') !== FALSE;
    if ($is_related || $is_relationship) {

      // Fall back the valid generic result since schemata is absent.
      return TRUE;
    }
    $entity_type_id = $resource_type
      ->getEntityTypeId();
    $bundle = $resource_type
      ->getBundle();
    $output_format = 'schema_json';
    $described_format = 'api_json';
    $schema_object = $this->schemaFactory
      ->create($entity_type_id, $bundle);
    $format = $output_format . ':' . $described_format;
    $output = $this->serializer
      ->serialize($schema_object, $format);
    $specific_schema = Json::decode($output);
    if (!$specific_schema) {
      return $is_valid;
    }

    // We need to individually validate each collection resource object.
    $is_collection = strpos($route_name, '.collection') !== FALSE;

    // Iterate over each resource object and check the schema.
    return array_reduce($is_collection ? $response_data->data : [
      $response_data->data,
    ], function ($valid, $resource_object) use ($specific_schema) {

      // Validating the schema first ensures that every object is processed.
      return $this
        ->validateSchema($specific_schema, $resource_object) && $valid;
    }, TRUE);
  }

  /**
   * Validates a string against a JSON Schema. It logs any possible errors.
   *
   * @param object $schema
   *   The JSON Schema object.
   * @param string $response_data
   *   The JSON string to validate.
   *
   * @return bool
   *   TRUE if the string is a valid instance of the schema. FALSE otherwise.
   */
  protected function validateSchema($schema, $response_data) {
    $this->validator
      ->check($response_data, $schema);
    $is_valid = $this->validator
      ->isValid();
    if (!$is_valid) {
      $this->logger
        ->debug("Response failed validation.\nResponse:\n@data\n\nErrors:\n@errors", [
        '@data' => Json::encode($response_data),
        '@errors' => Json::encode($this->validator
          ->getErrors()),
      ]);
    }
    return $is_valid;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ResourceResponseValidator::$appRoot protected property The application's root file path.
ResourceResponseValidator::$logger protected property The JSON API logger channel.
ResourceResponseValidator::$moduleHandler protected property The module handler.
ResourceResponseValidator::$schemaFactory protected property The schemata schema factory.
ResourceResponseValidator::$serializer protected property The serializer.
ResourceResponseValidator::$validator protected property The schema validator.
ResourceResponseValidator::doValidateResponse public function Wraps validation in an assert to prevent execution in production.
ResourceResponseValidator::getSubscribedEvents public static function Returns an array of event names this subscriber wants to listen to.
ResourceResponseValidator::onResponse public function Validates JSON API responses.
ResourceResponseValidator::setSchemaFactory public function Injects the schema factory.
ResourceResponseValidator::setValidator public function Sets the validator service if available.
ResourceResponseValidator::validateResponse protected function Validates a response against the JSON API specification.
ResourceResponseValidator::validateSchema protected function Validates a string against a JSON Schema. It logs any possible errors.
ResourceResponseValidator::__construct public function Constructs a ResourceResponseValidator object.