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