ebfd0750414e95512e21af492e3c89cf9c71e15e
[yaffs-website] / web / core / modules / rest / src / Plugin / rest / resource / EntityResource.php
1 <?php
2
3 namespace Drupal\rest\Plugin\rest\resource;
4
5 use Drupal\Component\Plugin\DependentPluginInterface;
6 use Drupal\Component\Plugin\PluginManagerInterface;
7 use Drupal\Core\Access\AccessResultReasonInterface;
8 use Drupal\Core\Cache\CacheableResponseInterface;
9 use Drupal\Core\Config\Entity\ConfigEntityType;
10 use Drupal\Core\Entity\EntityTypeManagerInterface;
11 use Drupal\Core\Entity\FieldableEntityInterface;
12 use Drupal\Core\Config\ConfigFactoryInterface;
13 use Drupal\Core\Entity\EntityInterface;
14 use Drupal\Core\Entity\EntityStorageException;
15 use Drupal\Core\Field\FieldItemListInterface;
16 use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException;
17 use Drupal\rest\Plugin\ResourceBase;
18 use Drupal\rest\ResourceResponse;
19 use Psr\Log\LoggerInterface;
20 use Symfony\Component\DependencyInjection\ContainerInterface;
21 use Drupal\rest\ModifiedResourceResponse;
22 use Symfony\Component\HttpFoundation\Response;
23 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
24 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
25 use Symfony\Component\HttpKernel\Exception\HttpException;
26
27 /**
28  * Represents entities as resources.
29  *
30  * @see \Drupal\rest\Plugin\Deriver\EntityDeriver
31  *
32  * @RestResource(
33  *   id = "entity",
34  *   label = @Translation("Entity"),
35  *   serialization_class = "Drupal\Core\Entity\Entity",
36  *   deriver = "Drupal\rest\Plugin\Deriver\EntityDeriver",
37  *   uri_paths = {
38  *     "canonical" = "/entity/{entity_type}/{entity}",
39  *     "create" = "/entity/{entity_type}"
40  *   }
41  * )
42  */
43 class EntityResource extends ResourceBase implements DependentPluginInterface {
44
45   use EntityResourceValidationTrait;
46   use EntityResourceAccessTrait;
47
48   /**
49    * The entity type targeted by this resource.
50    *
51    * @var \Drupal\Core\Entity\EntityTypeInterface
52    */
53   protected $entityType;
54
55   /**
56    * The config factory.
57    *
58    * @var \Drupal\Core\Config\ConfigFactoryInterface
59    */
60   protected $configFactory;
61
62   /**
63    * The link relation type manager used to create HTTP header links.
64    *
65    * @var \Drupal\Component\Plugin\PluginManagerInterface
66    */
67   protected $linkRelationTypeManager;
68
69   /**
70    * Constructs a Drupal\rest\Plugin\rest\resource\EntityResource object.
71    *
72    * @param array $configuration
73    *   A configuration array containing information about the plugin instance.
74    * @param string $plugin_id
75    *   The plugin_id for the plugin instance.
76    * @param mixed $plugin_definition
77    *   The plugin implementation definition.
78    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
79    *   The entity type manager
80    * @param array $serializer_formats
81    *   The available serialization formats.
82    * @param \Psr\Log\LoggerInterface $logger
83    *   A logger instance.
84    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
85    *   The config factory.
86    * @param \Drupal\Component\Plugin\PluginManagerInterface $link_relation_type_manager
87    *   The link relation type manager.
88    */
89   public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, $serializer_formats, LoggerInterface $logger, ConfigFactoryInterface $config_factory, PluginManagerInterface $link_relation_type_manager) {
90     parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
91     $this->entityType = $entity_type_manager->getDefinition($plugin_definition['entity_type']);
92     $this->configFactory = $config_factory;
93     $this->linkRelationTypeManager = $link_relation_type_manager;
94   }
95
96   /**
97    * {@inheritdoc}
98    */
99   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
100     return new static(
101       $configuration,
102       $plugin_id,
103       $plugin_definition,
104       $container->get('entity_type.manager'),
105       $container->getParameter('serializer.formats'),
106       $container->get('logger.factory')->get('rest'),
107       $container->get('config.factory'),
108       $container->get('plugin.manager.link_relation_type')
109     );
110   }
111
112   /**
113    * Responds to entity GET requests.
114    *
115    * @param \Drupal\Core\Entity\EntityInterface $entity
116    *   The entity object.
117    *
118    * @return \Drupal\rest\ResourceResponse
119    *   The response containing the entity with its accessible fields.
120    *
121    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
122    */
123   public function get(EntityInterface $entity) {
124     $entity_access = $entity->access('view', NULL, TRUE);
125     if (!$entity_access->isAllowed()) {
126       throw new CacheableAccessDeniedHttpException($entity_access, $entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
127     }
128
129     $response = new ResourceResponse($entity, 200);
130     $response->addCacheableDependency($entity);
131     $response->addCacheableDependency($entity_access);
132
133     if ($entity instanceof FieldableEntityInterface) {
134       foreach ($entity as $field_name => $field) {
135         /** @var \Drupal\Core\Field\FieldItemListInterface $field */
136         $field_access = $field->access('view', NULL, TRUE);
137         $response->addCacheableDependency($field_access);
138
139         if (!$field_access->isAllowed()) {
140           $entity->set($field_name, NULL);
141         }
142       }
143     }
144
145     $this->addLinkHeaders($entity, $response);
146
147     return $response;
148   }
149
150   /**
151    * Responds to entity POST requests and saves the new entity.
152    *
153    * @param \Drupal\Core\Entity\EntityInterface $entity
154    *   The entity.
155    *
156    * @return \Drupal\rest\ModifiedResourceResponse
157    *   The HTTP response object.
158    *
159    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
160    */
161   public function post(EntityInterface $entity = NULL) {
162     if ($entity == NULL) {
163       throw new BadRequestHttpException('No entity content received.');
164     }
165
166     $entity_access = $entity->access('create', NULL, TRUE);
167     if (!$entity_access->isAllowed()) {
168       throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'create'));
169     }
170     $definition = $this->getPluginDefinition();
171     // Verify that the deserialized entity is of the type that we expect to
172     // prevent security issues.
173     if ($entity->getEntityTypeId() != $definition['entity_type']) {
174       throw new BadRequestHttpException('Invalid entity type');
175     }
176     // POSTed entities must not have an ID set, because we always want to create
177     // new entities here.
178     if (!$entity->isNew()) {
179       throw new BadRequestHttpException('Only new entities can be created');
180     }
181
182     $this->checkEditFieldAccess($entity);
183
184     // Validate the received data before saving.
185     $this->validate($entity);
186     try {
187       $entity->save();
188       $this->logger->notice('Created entity %type with ID %id.', ['%type' => $entity->getEntityTypeId(), '%id' => $entity->id()]);
189
190       // 201 Created responses return the newly created entity in the response
191       // body. These responses are not cacheable, so we add no cacheability
192       // metadata here.
193       $headers = [];
194       if (in_array('canonical', $entity->uriRelationships(), TRUE)) {
195         $url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE);
196         $headers['Location'] = $url->getGeneratedUrl();
197       }
198       return new ModifiedResourceResponse($entity, 201, $headers);
199     }
200     catch (EntityStorageException $e) {
201       throw new HttpException(500, 'Internal Server Error', $e);
202     }
203   }
204
205   /**
206    * Responds to entity PATCH requests.
207    *
208    * @param \Drupal\Core\Entity\EntityInterface $original_entity
209    *   The original entity object.
210    * @param \Drupal\Core\Entity\EntityInterface $entity
211    *   The entity.
212    *
213    * @return \Drupal\rest\ModifiedResourceResponse
214    *   The HTTP response object.
215    *
216    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
217    */
218   public function patch(EntityInterface $original_entity, EntityInterface $entity = NULL) {
219     if ($entity == NULL) {
220       throw new BadRequestHttpException('No entity content received.');
221     }
222     $definition = $this->getPluginDefinition();
223     if ($entity->getEntityTypeId() != $definition['entity_type']) {
224       throw new BadRequestHttpException('Invalid entity type');
225     }
226     $entity_access = $original_entity->access('update', NULL, TRUE);
227     if (!$entity_access->isAllowed()) {
228       throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'update'));
229     }
230
231     // Overwrite the received fields.
232     // @todo Remove $changed_fields in https://www.drupal.org/project/drupal/issues/2862574.
233     $changed_fields = [];
234     foreach ($entity->_restSubmittedFields as $field_name) {
235       $field = $entity->get($field_name);
236       // It is not possible to set the language to NULL as it is automatically
237       // re-initialized. As it must not be empty, skip it if it is.
238       // @todo Remove in https://www.drupal.org/project/drupal/issues/2933408.
239       if ($entity->getEntityType()->hasKey('langcode') && $field_name === $entity->getEntityType()->getKey('langcode') && $field->isEmpty()) {
240         continue;
241       }
242       if ($this->checkPatchFieldAccess($original_entity->get($field_name), $field)) {
243         $changed_fields[] = $field_name;
244         $original_entity->set($field_name, $field->getValue());
245       }
246     }
247
248     // If no fields are changed, we can send a response immediately!
249     if (empty($changed_fields)) {
250       return new ModifiedResourceResponse($original_entity, 200);
251     }
252
253     // Validate the received data before saving.
254     $this->validate($original_entity, $changed_fields);
255     try {
256       $original_entity->save();
257       $this->logger->notice('Updated entity %type with ID %id.', ['%type' => $original_entity->getEntityTypeId(), '%id' => $original_entity->id()]);
258
259       // Return the updated entity in the response body.
260       return new ModifiedResourceResponse($original_entity, 200);
261     }
262     catch (EntityStorageException $e) {
263       throw new HttpException(500, 'Internal Server Error', $e);
264     }
265   }
266
267   /**
268    * Checks whether the given field should be PATCHed.
269    *
270    * @param \Drupal\Core\Field\FieldItemListInterface $original_field
271    *   The original (stored) value for the field.
272    * @param \Drupal\Core\Field\FieldItemListInterface $received_field
273    *   The received value for the field.
274    *
275    * @return bool
276    *   Whether the field should be PATCHed or not.
277    *
278    * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
279    *   Thrown when the user sending the request is not allowed to update the
280    *   field. Only thrown when the user could not abuse this information to
281    *   determine the stored value.
282    *
283    * @internal
284    */
285   protected function checkPatchFieldAccess(FieldItemListInterface $original_field, FieldItemListInterface $received_field) {
286     // The user might not have access to edit the field, but still needs to
287     // submit the current field value as part of the PATCH request. For
288     // example, the entity keys required by denormalizers. Therefore, if the
289     // received value equals the stored value, return FALSE without throwing an
290     // exception. But only for fields that the user has access to view, because
291     // the user has no legitimate way of knowing the current value of fields
292     // that they are not allowed to view, and we must not make the presence or
293     // absence of a 403 response a way to find that out.
294     if ($original_field->access('view') && $original_field->equals($received_field)) {
295       return FALSE;
296     }
297
298     // If the user is allowed to edit the field, it is always safe to set the
299     // received value. We may be setting an unchanged value, but that is ok.
300     $field_edit_access = $original_field->access('edit', NULL, TRUE);
301     if ($field_edit_access->isAllowed()) {
302       return TRUE;
303     }
304
305     // It's helpful and safe to let the user know when they are not allowed to
306     // update a field.
307     $field_name = $received_field->getName();
308     $error_message = "Access denied on updating field '$field_name'.";
309     if ($field_edit_access instanceof AccessResultReasonInterface) {
310       $reason = $field_edit_access->getReason();
311       if ($reason) {
312         $error_message .= ' ' . $reason;
313       }
314     }
315     throw new AccessDeniedHttpException($error_message);
316   }
317
318   /**
319    * Responds to entity DELETE requests.
320    *
321    * @param \Drupal\Core\Entity\EntityInterface $entity
322    *   The entity object.
323    *
324    * @return \Drupal\rest\ModifiedResourceResponse
325    *   The HTTP response object.
326    *
327    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
328    */
329   public function delete(EntityInterface $entity) {
330     $entity_access = $entity->access('delete', NULL, TRUE);
331     if (!$entity_access->isAllowed()) {
332       throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'delete'));
333     }
334     try {
335       $entity->delete();
336       $this->logger->notice('Deleted entity %type with ID %id.', ['%type' => $entity->getEntityTypeId(), '%id' => $entity->id()]);
337
338       // DELETE responses have an empty body.
339       return new ModifiedResourceResponse(NULL, 204);
340     }
341     catch (EntityStorageException $e) {
342       throw new HttpException(500, 'Internal Server Error', $e);
343     }
344   }
345
346   /**
347    * Generates a fallback access denied message, when no specific reason is set.
348    *
349    * @param \Drupal\Core\Entity\EntityInterface $entity
350    *   The entity object.
351    * @param string $operation
352    *   The disallowed entity operation.
353    *
354    * @return string
355    *   The proper message to display in the AccessDeniedHttpException.
356    */
357   protected function generateFallbackAccessDeniedMessage(EntityInterface $entity, $operation) {
358     $message = "You are not authorized to {$operation} this {$entity->getEntityTypeId()} entity";
359
360     if ($entity->bundle() !== $entity->getEntityTypeId()) {
361       $message .= " of bundle {$entity->bundle()}";
362     }
363     return "{$message}.";
364   }
365
366   /**
367    * {@inheritdoc}
368    */
369   public function permissions() {
370     // @see https://www.drupal.org/node/2664780
371     if ($this->configFactory->get('rest.settings')->get('bc_entity_resource_permissions')) {
372       // The default Drupal 8.0.x and 8.1.x behavior.
373       return parent::permissions();
374     }
375     else {
376       // The default Drupal 8.2.x behavior.
377       return [];
378     }
379   }
380
381   /**
382    * {@inheritdoc}
383    */
384   protected function getBaseRoute($canonical_path, $method) {
385     $route = parent::getBaseRoute($canonical_path, $method);
386     $definition = $this->getPluginDefinition();
387
388     $parameters = $route->getOption('parameters') ?: [];
389     $parameters[$definition['entity_type']]['type'] = 'entity:' . $definition['entity_type'];
390     $route->setOption('parameters', $parameters);
391
392     return $route;
393   }
394
395   /**
396    * {@inheritdoc}
397    */
398   public function availableMethods() {
399     $methods = parent::availableMethods();
400     if ($this->isConfigEntityResource()) {
401       // Currently only GET is supported for Config Entities.
402       // @todo Remove when supported https://www.drupal.org/node/2300677
403       $unsupported_methods = ['POST', 'PUT', 'DELETE', 'PATCH'];
404       $methods = array_diff($methods, $unsupported_methods);
405     }
406     return $methods;
407   }
408
409   /**
410    * Checks if this resource is for a Config Entity.
411    *
412    * @return bool
413    *   TRUE if the entity is a Config Entity, FALSE otherwise.
414    */
415   protected function isConfigEntityResource() {
416     return $this->entityType instanceof ConfigEntityType;
417   }
418
419   /**
420    * {@inheritdoc}
421    */
422   public function calculateDependencies() {
423     if (isset($this->entityType)) {
424       return ['module' => [$this->entityType->getProvider()]];
425     }
426   }
427
428   /**
429    * Adds link headers to a response.
430    *
431    * @param \Drupal\Core\Entity\EntityInterface $entity
432    *   The entity.
433    * @param \Symfony\Component\HttpFoundation\Response $response
434    *   The response.
435    *
436    * @see https://tools.ietf.org/html/rfc5988#section-5
437    */
438   protected function addLinkHeaders(EntityInterface $entity, Response $response) {
439     foreach ($entity->uriRelationships() as $relation_name) {
440       if ($this->linkRelationTypeManager->hasDefinition($relation_name)) {
441         /** @var \Drupal\Core\Http\LinkRelationTypeInterface $link_relation_type */
442         $link_relation_type = $this->linkRelationTypeManager->createInstance($relation_name);
443
444         $generator_url = $entity->toUrl($relation_name)
445           ->setAbsolute(TRUE)
446           ->toString(TRUE);
447         if ($response instanceof CacheableResponseInterface) {
448           $response->addCacheableDependency($generator_url);
449         }
450         $uri = $generator_url->getGeneratedUrl();
451
452         $relationship = $link_relation_type->isRegistered()
453           ? $link_relation_type->getRegisteredName()
454           : $link_relation_type->getExtensionUri();
455
456         $link_header = '<' . $uri . '>; rel="' . $relationship . '"';
457         $response->headers->set('Link', $link_header, FALSE);
458       }
459     }
460   }
461
462 }