summaryrefslogtreecommitdiffstats
path: root/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
diff options
context:
space:
mode:
Diffstat (limited to 'core/modules/rest/src/Plugin/rest/resource/EntityResource.php')
-rw-r--r--core/modules/rest/src/Plugin/rest/resource/EntityResource.php137
1 files changed, 71 insertions, 66 deletions
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 5d9849d..ebfd075 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -4,6 +4,7 @@ namespace Drupal\rest\Plugin\rest\resource;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\Core\Entity\EntityTypeManagerInterface;
@@ -12,7 +13,7 @@ use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\TypedData\PrimitiveInterface;
+use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Psr\Log\LoggerInterface;
@@ -122,7 +123,7 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
public function get(EntityInterface $entity) {
$entity_access = $entity->access('view', NULL, TRUE);
if (!$entity_access->isAllowed()) {
- throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
+ throw new CacheableAccessDeniedHttpException($entity_access, $entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
}
$response = new ResourceResponse($entity, 200);
@@ -202,41 +203,6 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
}
/**
- * Gets the values from the field item list casted to the correct type.
- *
- * Values are casted to the correct type so we can determine whether or not
- * something has changed. REST formats such as JSON support typed data but
- * Drupal's database API will return values as strings. Currently, only
- * primitive data types know how to cast their values to the correct type.
- *
- * @param \Drupal\Core\Field\FieldItemListInterface $field_item_list
- * The field item list to retrieve its data from.
- *
- * @return mixed[][]
- * The values from the field item list casted to the correct type. The array
- * of values returned is a multidimensional array keyed by delta and the
- * property name.
- */
- protected function getCastedValueFromFieldItemList(FieldItemListInterface $field_item_list) {
- $value = $field_item_list->getValue();
-
- foreach ($value as $delta => $field_item_value) {
- /** @var \Drupal\Core\Field\FieldItemInterface $field_item */
- $field_item = $field_item_list->get($delta);
- $properties = $field_item->getProperties(TRUE);
- // Foreach field value we check whether we know the underlying property.
- // If we exists we try to cast the value.
- foreach ($field_item_value as $property_name => $property_value) {
- if (isset($properties[$property_name]) && ($property = $field_item->get($property_name)) && $property instanceof PrimitiveInterface) {
- $value[$delta][$property_name] = $property->getCastedValue();
- }
- }
- }
-
- return $value;
- }
-
- /**
* Responds to entity PATCH requests.
*
* @param \Drupal\Core\Entity\EntityInterface $original_entity
@@ -262,42 +228,30 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'update'));
}
- // Overwrite the received properties.
- $entity_keys = $entity->getEntityType()->getKeys();
+ // Overwrite the received fields.
+ // @todo Remove $changed_fields in https://www.drupal.org/project/drupal/issues/2862574.
+ $changed_fields = [];
foreach ($entity->_restSubmittedFields as $field_name) {
$field = $entity->get($field_name);
-
- // Entity key fields need special treatment: together they uniquely
- // identify the entity. Therefore it does not make sense to modify any of
- // them. However, rather than throwing an error, we just ignore them as
- // long as their specified values match their current values.
- if (in_array($field_name, $entity_keys, TRUE)) {
- // @todo Work around the wrong assumption that entity keys need special
- // treatment, when only read-only fields need it.
- // This will be fixed in https://www.drupal.org/node/2824851.
- if ($entity->getEntityTypeId() == 'comment' && $field_name == 'status' && !$original_entity->get($field_name)->access('edit')) {
- throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
- }
-
- // Unchanged values for entity keys don't need access checking.
- if ($this->getCastedValueFromFieldItemList($original_entity->get($field_name)) === $this->getCastedValueFromFieldItemList($entity->get($field_name))) {
- continue;
- }
- // It is not possible to set the language to NULL as it is automatically
- // re-initialized. As it must not be empty, skip it if it is.
- elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) {
- continue;
- }
+ // It is not possible to set the language to NULL as it is automatically
+ // re-initialized. As it must not be empty, skip it if it is.
+ // @todo Remove in https://www.drupal.org/project/drupal/issues/2933408.
+ if ($entity->getEntityType()->hasKey('langcode') && $field_name === $entity->getEntityType()->getKey('langcode') && $field->isEmpty()) {
+ continue;
}
-
- if (!$original_entity->get($field_name)->access('edit')) {
- throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
+ if ($this->checkPatchFieldAccess($original_entity->get($field_name), $field)) {
+ $changed_fields[] = $field_name;
+ $original_entity->set($field_name, $field->getValue());
}
- $original_entity->set($field_name, $field->getValue());
+ }
+
+ // If no fields are changed, we can send a response immediately!
+ if (empty($changed_fields)) {
+ return new ModifiedResourceResponse($original_entity, 200);
}
// Validate the received data before saving.
- $this->validate($original_entity);
+ $this->validate($original_entity, $changed_fields);
try {
$original_entity->save();
$this->logger->notice('Updated entity %type with ID %id.', ['%type' => $original_entity->getEntityTypeId(), '%id' => $original_entity->id()]);
@@ -311,6 +265,57 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
}
/**
+ * Checks whether the given field should be PATCHed.
+ *
+ * @param \Drupal\Core\Field\FieldItemListInterface $original_field
+ * The original (stored) value for the field.
+ * @param \Drupal\Core\Field\FieldItemListInterface $received_field
+ * The received value for the field.
+ *
+ * @return bool
+ * Whether the field should be PATCHed or not.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
+ * Thrown when the user sending the request is not allowed to update the
+ * field. Only thrown when the user could not abuse this information to
+ * determine the stored value.
+ *
+ * @internal
+ */
+ protected function checkPatchFieldAccess(FieldItemListInterface $original_field, FieldItemListInterface $received_field) {
+ // The user might not have access to edit the field, but still needs to
+ // submit the current field value as part of the PATCH request. For
+ // example, the entity keys required by denormalizers. Therefore, if the
+ // received value equals the stored value, return FALSE without throwing an
+ // exception. But only for fields that the user has access to view, because
+ // the user has no legitimate way of knowing the current value of fields
+ // that they are not allowed to view, and we must not make the presence or
+ // absence of a 403 response a way to find that out.
+ if ($original_field->access('view') && $original_field->equals($received_field)) {
+ return FALSE;
+ }
+
+ // If the user is allowed to edit the field, it is always safe to set the
+ // received value. We may be setting an unchanged value, but that is ok.
+ $field_edit_access = $original_field->access('edit', NULL, TRUE);
+ if ($field_edit_access->isAllowed()) {
+ return TRUE;
+ }
+
+ // It's helpful and safe to let the user know when they are not allowed to
+ // update a field.
+ $field_name = $received_field->getName();
+ $error_message = "Access denied on updating field '$field_name'.";
+ if ($field_edit_access instanceof AccessResultReasonInterface) {
+ $reason = $field_edit_access->getReason();
+ if ($reason) {
+ $error_message .= ' ' . $reason;
+ }
+ }
+ throw new AccessDeniedHttpException($error_message);
+ }
+
+ /**
* Responds to entity DELETE requests.
*
* @param \Drupal\Core\Entity\EntityInterface $entity