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