annotate core/modules/jsonapi/src/EventSubscriber/ResourceResponseValidator.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@18 1 <?php
Chris@18 2
Chris@18 3 namespace Drupal\jsonapi\EventSubscriber;
Chris@18 4
Chris@18 5 use Drupal\Component\Serialization\Json;
Chris@18 6 use Drupal\Core\Extension\ModuleHandlerInterface;
Chris@18 7 use JsonSchema\Validator;
Chris@18 8 use Psr\Log\LoggerInterface;
Chris@18 9 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
Chris@18 10 use Symfony\Component\HttpFoundation\Request;
Chris@18 11 use Symfony\Component\HttpFoundation\Response;
Chris@18 12 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
Chris@18 13 use Symfony\Component\HttpKernel\KernelEvents;
Chris@18 14 use Symfony\Component\Serializer\SerializerInterface;
Chris@18 15
Chris@18 16 /**
Chris@18 17 * Response subscriber that validates a JSON:API response.
Chris@18 18 *
Chris@18 19 * This must run after ResourceResponseSubscriber.
Chris@18 20 *
Chris@18 21 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
Chris@18 22 * may change at any time and could break any dependencies on it.
Chris@18 23 *
Chris@18 24 * @see https://www.drupal.org/project/jsonapi/issues/3032787
Chris@18 25 * @see jsonapi.api.php
Chris@18 26 *
Chris@18 27 * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
Chris@18 28 */
Chris@18 29 class ResourceResponseValidator implements EventSubscriberInterface {
Chris@18 30
Chris@18 31 /**
Chris@18 32 * The serializer.
Chris@18 33 *
Chris@18 34 * @var \Symfony\Component\Serializer\SerializerInterface
Chris@18 35 */
Chris@18 36 protected $serializer;
Chris@18 37
Chris@18 38 /**
Chris@18 39 * The JSON:API logger channel.
Chris@18 40 *
Chris@18 41 * @var \Psr\Log\LoggerInterface
Chris@18 42 */
Chris@18 43 protected $logger;
Chris@18 44
Chris@18 45 /**
Chris@18 46 * The schema validator.
Chris@18 47 *
Chris@18 48 * This property will only be set if the validator library is available.
Chris@18 49 *
Chris@18 50 * @var \JsonSchema\Validator|null
Chris@18 51 */
Chris@18 52 protected $validator;
Chris@18 53
Chris@18 54 /**
Chris@18 55 * The module handler.
Chris@18 56 *
Chris@18 57 * @var \Drupal\Core\Extension\ModuleHandlerInterface
Chris@18 58 */
Chris@18 59 protected $moduleHandler;
Chris@18 60
Chris@18 61 /**
Chris@18 62 * The application's root file path.
Chris@18 63 *
Chris@18 64 * @var string
Chris@18 65 */
Chris@18 66 protected $appRoot;
Chris@18 67
Chris@18 68 /**
Chris@18 69 * Constructs a ResourceResponseValidator object.
Chris@18 70 *
Chris@18 71 * @param \Symfony\Component\Serializer\SerializerInterface $serializer
Chris@18 72 * The serializer.
Chris@18 73 * @param \Psr\Log\LoggerInterface $logger
Chris@18 74 * The JSON:API logger channel.
Chris@18 75 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
Chris@18 76 * The module handler.
Chris@18 77 * @param string $app_root
Chris@18 78 * The application's root file path.
Chris@18 79 */
Chris@18 80 public function __construct(SerializerInterface $serializer, LoggerInterface $logger, ModuleHandlerInterface $module_handler, $app_root) {
Chris@18 81 $this->serializer = $serializer;
Chris@18 82 $this->logger = $logger;
Chris@18 83 $this->moduleHandler = $module_handler;
Chris@18 84 $this->appRoot = $app_root;
Chris@18 85 }
Chris@18 86
Chris@18 87 /**
Chris@18 88 * {@inheritdoc}
Chris@18 89 */
Chris@18 90 public static function getSubscribedEvents() {
Chris@18 91 $events[KernelEvents::RESPONSE][] = ['onResponse'];
Chris@18 92 return $events;
Chris@18 93 }
Chris@18 94
Chris@18 95 /**
Chris@18 96 * Sets the validator service if available.
Chris@18 97 */
Chris@18 98 public function setValidator(Validator $validator = NULL) {
Chris@18 99 if ($validator) {
Chris@18 100 $this->validator = $validator;
Chris@18 101 }
Chris@18 102 elseif (class_exists(Validator::class)) {
Chris@18 103 $this->validator = new Validator();
Chris@18 104 }
Chris@18 105 }
Chris@18 106
Chris@18 107 /**
Chris@18 108 * Validates JSON:API responses.
Chris@18 109 *
Chris@18 110 * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
Chris@18 111 * The event to process.
Chris@18 112 */
Chris@18 113 public function onResponse(FilterResponseEvent $event) {
Chris@18 114 $response = $event->getResponse();
Chris@18 115 if (strpos($response->headers->get('Content-Type'), 'application/vnd.api+json') === FALSE) {
Chris@18 116 return;
Chris@18 117 }
Chris@18 118
Chris@18 119 $this->doValidateResponse($response, $event->getRequest());
Chris@18 120 }
Chris@18 121
Chris@18 122 /**
Chris@18 123 * Wraps validation in an assert to prevent execution in production.
Chris@18 124 *
Chris@18 125 * @see self::validateResponse
Chris@18 126 */
Chris@18 127 public function doValidateResponse(Response $response, Request $request) {
Chris@18 128 if (PHP_MAJOR_VERSION >= 7 || assert_options(ASSERT_ACTIVE)) {
Chris@18 129 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');
Chris@18 130 }
Chris@18 131 }
Chris@18 132
Chris@18 133 /**
Chris@18 134 * Validates a response against the JSON:API specification.
Chris@18 135 *
Chris@18 136 * @param \Symfony\Component\HttpFoundation\Response $response
Chris@18 137 * The response to validate.
Chris@18 138 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@18 139 * The request containing info about what to validate.
Chris@18 140 *
Chris@18 141 * @return bool
Chris@18 142 * FALSE if the response failed validation, otherwise TRUE.
Chris@18 143 */
Chris@18 144 protected function validateResponse(Response $response, Request $request) {
Chris@18 145 // If the validator isn't set, then the validation library is not installed.
Chris@18 146 if (!$this->validator) {
Chris@18 147 return TRUE;
Chris@18 148 }
Chris@18 149
Chris@18 150 // Do not use Json::decode here since it coerces the response into an
Chris@18 151 // associative array, which creates validation errors.
Chris@18 152 $response_data = json_decode($response->getContent());
Chris@18 153 if (empty($response_data)) {
Chris@18 154 return TRUE;
Chris@18 155 }
Chris@18 156
Chris@18 157 $schema_ref = sprintf(
Chris@18 158 'file://%s/schema.json',
Chris@18 159 implode('/', [
Chris@18 160 $this->appRoot,
Chris@18 161 $this->moduleHandler->getModule('jsonapi')->getPath(),
Chris@18 162 ])
Chris@18 163 );
Chris@18 164 $generic_jsonapi_schema = (object) ['$ref' => $schema_ref];
Chris@18 165
Chris@18 166 return $this->validateSchema($generic_jsonapi_schema, $response_data);
Chris@18 167 }
Chris@18 168
Chris@18 169 /**
Chris@18 170 * Validates a string against a JSON Schema. It logs any possible errors.
Chris@18 171 *
Chris@18 172 * @param object $schema
Chris@18 173 * The JSON Schema object.
Chris@18 174 * @param string $response_data
Chris@18 175 * The JSON string to validate.
Chris@18 176 *
Chris@18 177 * @return bool
Chris@18 178 * TRUE if the string is a valid instance of the schema. FALSE otherwise.
Chris@18 179 */
Chris@18 180 protected function validateSchema($schema, $response_data) {
Chris@18 181 $this->validator->check($response_data, $schema);
Chris@18 182 $is_valid = $this->validator->isValid();
Chris@18 183 if (!$is_valid) {
Chris@18 184 $this->logger->debug("Response failed validation.\nResponse:\n@data\n\nErrors:\n@errors", [
Chris@18 185 '@data' => Json::encode($response_data),
Chris@18 186 '@errors' => Json::encode($this->validator->getErrors()),
Chris@18 187 ]);
Chris@18 188 }
Chris@18 189 return $is_valid;
Chris@18 190 }
Chris@18 191
Chris@18 192 }