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 }
|