Mercurial > hg > cmmr2012-drupal-site
diff core/modules/jsonapi/src/EventSubscriber/ResourceResponseValidator.php @ 5:12f9dff5fda9 tip
Update to Drupal core 8.7.1
author | Chris Cannam |
---|---|
date | Thu, 09 May 2019 15:34:47 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/core/modules/jsonapi/src/EventSubscriber/ResourceResponseValidator.php Thu May 09 15:34:47 2019 +0100 @@ -0,0 +1,192 @@ +<?php + +namespace Drupal\jsonapi\EventSubscriber; + +use Drupal\Component\Serialization\Json; +use Drupal\Core\Extension\ModuleHandlerInterface; +use JsonSchema\Validator; +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * Response subscriber that validates a JSON:API response. + * + * This must run after ResourceResponseSubscriber. + * + * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class + * may change at any time and could break any dependencies on it. + * + * @see https://www.drupal.org/project/jsonapi/issues/3032787 + * @see jsonapi.api.php + * + * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber + */ +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 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(); + } + } + + /** + * Validates JSON:API responses. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ + public function onResponse(FilterResponseEvent $event) { + $response = $event->getResponse(); + if (strpos($response->headers->get('Content-Type'), 'application/vnd.api+json') === FALSE) { + 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]; + + return $this->validateSchema($generic_jsonapi_schema, $response_data); + } + + /** + * 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; + } + +}