Commit 829fe8a2 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #2631774 by Wim Leers, marthinal, dawehner, tedbow, tyler.frankenstein,...

Issue #2631774 by Wim Leers, marthinal, dawehner, tedbow, tyler.frankenstein, gabesullice, valthebald: Impossible to update Comment entity with REST (HTTP PATCH): bundle field not allowed to be updated, but EntityNormalizer::denormalize() requires it

(cherry picked from commit 6749853b)
parent 11d4d9fd
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -233,9 +233,12 @@ function rdf_comment_storage_load($comments) {
    // to optimize performance for websites that implement an entity cache.
    $created_mapping = rdf_get_mapping('comment', $comment->bundle())
      ->getPreparedFieldMapping('created');
    /** @var \Drupal\comment\CommentInterface $comment*/
    $comment->rdf_data['date'] = rdf_rdfa_attributes($created_mapping, $comment->get('created')->first()->toArray());
    $entity = $comment->getCommentedEntity();
    $comment->rdf_data['entity_uri'] = $entity->url();
    // The current function is a storage level hook, so avoid to bubble
    // bubbleable metadata, because it can be outside of a render context.
    $comment->rdf_data['entity_uri'] = $entity->toUrl()->toString(TRUE)->getGeneratedUrl();
    if ($comment->hasParentComment()) {
      $comment->rdf_data['pid_uri'] = $comment->getParentComment()->url();
    }
+16 −5
Original line number Diff line number Diff line
@@ -146,14 +146,25 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    }

    // Overwrite the received properties.
    $langcode_key = $entity->getEntityType()->getKey('langcode');
    $entity_keys = $entity->getEntityType()->getKeys();
    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)) {
        // Unchanged values for entity keys don't need access checking.
        if ($original_entity->get($field_name)->getValue() === $entity->get($field_name)->getValue()) {
          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.
      if ($field_name == $langcode_key && $field->isEmpty()) {
        elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) {
          continue;
        }
      }

      if (!$original_entity->get($field_name)->access('edit')) {
        throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
+10 −5
Original line number Diff line number Diff line
@@ -250,8 +250,8 @@ protected function entityValues($entity_type) {
   *   resource types.
   * @param string $method
   *   The HTTP method to enable, e.g. GET, POST etc.
   * @param string $format
   *   (Optional) The serialization format, e.g. hal_json.
   * @param string|array $format
   *   (Optional) The serialization format, e.g. hal_json, or a list of formats.
   * @param array $auth
   *   (Optional) The list of valid authentication methods.
   */
@@ -261,10 +261,15 @@ protected function enableService($resource_type, $method = 'GET', $format = NULL
    $settings = array();

    if ($resource_type) {
      if (is_array($format)) {
        $settings[$resource_type][$method]['supported_formats'] = $format;
      }
      else {
        if ($format == NULL) {
          $format = $this->defaultFormat;
        }
        $settings[$resource_type][$method]['supported_formats'][] = $format;
      }

      if ($auth == NULL) {
        $auth = $this->defaultAuth;
+138 −1
Original line number Diff line number Diff line
@@ -2,7 +2,12 @@

namespace Drupal\rest\Tests;

use Drupal\comment\Entity\Comment;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\entity_test\Entity\EntityTest;

/**
 * Tests the update of resources.
@@ -11,12 +16,22 @@
 */
class UpdateTest extends RESTTestBase {

  use CommentTestTrait;

  /**
   * Modules to install.
   *
   * @var array
   */
  public static $modules = array('hal', 'rest', 'entity_test');
  public static $modules = ['hal', 'rest', 'entity_test', 'comment'];

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    parent::setUp();
    $this->addDefaultCommentField('entity_test', 'entity_test');
  }

  /**
   * Tests several valid and invalid partial update requests on test entities.
@@ -220,7 +235,129 @@ public function testUpdateUser() {
    // Verify that we can log in with the new password.
    $account->pass_raw = $new_password;
    $this->drupalLogin($account);
  }

  /**
   * Test patching a comment using both HAL+JSON and JSON.
   */
  public function testUpdateComment() {
    $entity_type = 'comment';
    // Enables the REST service for 'comment' entity type.
    $this->enableService('entity:' . $entity_type, 'PATCH', ['hal_json', 'json']);
    $permissions = $this->entityPermissions($entity_type, 'update');
    $permissions[] = 'restful patch entity:' . $entity_type;
    $account = $this->drupalCreateUser($permissions);
    $account->set('mail', 'old-email@example.com');
    $this->drupalLogin($account);

    // Create & save an entity to comment on, plus a comment.
    $entity_test = EntityTest::create();
    $entity_test->save();
    $entity_values = $this->entityValues($entity_type);
    $entity_values['entity_id'] = $entity_test->id();
    $entity_values['uid'] = $account->id();
    $comment = Comment::create($entity_values);
    $comment->save();

    $this->pass('Test case 1: PATCH comment using HAL+JSON.');
    $comment->setSubject('Initial subject')->save();
    $read_only_fields = [
      'name',
      'created',
      'changed',
      'status',
      'thread',
      'entity_type',
      'field_name',
      'entity_id',
      'uid',
    ];
    $this->assertNotEqual('Updated subject', $comment->getSubject());
    $comment->setSubject('Updated subject');
    $this->patchEntity($comment, $read_only_fields, $account, 'hal_json', 'application/hal+json');
    $comment = Comment::load($comment->id());
    $this->assertEqual('Updated subject', $comment->getSubject());

    $this->pass('Test case 1: PATCH comment using JSON.');
    $comment->setSubject('Initial subject')->save();
    $read_only_fields = [
      'pid', // Extra compared to HAL+JSON.
      'entity_id',
      'uid',
      'name',
      'homepage', // Extra compared to HAL+JSON.
      'created',
      'changed',
      'status',
      'thread',
      'entity_type',
      'field_name',
    ];
    $this->assertNotEqual('Updated subject', $comment->getSubject());
    $comment->setSubject('Updated subject');
    $this->patchEntity($comment, $read_only_fields, $account, 'json', 'application/json');
    $comment = Comment::load($comment->id());
    $this->assertEqual('Updated subject', $comment->getSubject());
  }

  /**
   * Patches an existing entity using the passed in (modified) entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The updated entity to send.
   * @param string[] $read_only_fields
   *   Names of the fields that are read-only, in validation order.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The account to use for serialization.
   * @param string $format
   *   A serialization format.
   * @param string $mime_type
   *   The MIME type corresponding to the specified serialization format.
   */
  protected function patchEntity(EntityInterface $entity, array $read_only_fields, AccountInterface $account, $format, $mime_type) {
    $serializer = $this->container->get('serializer');

    $url = $entity->toUrl();
    $context = ['account' => $account];
    // Certain fields are always read-only, others this user simply is not
    // allowed to modify. For all of them, ensure they are not serialized, else
    // we'll get a 403 plus an error message.
    for ($i = 0; $i < count($read_only_fields); $i++) {
      $field = $read_only_fields[$i];

      $normalized = $serializer->normalize($entity, $format, $context);
      if ($format !== 'hal_json') {
        // The default normalizer always keeps fields, even if they are unset
        // here because they should be omitted during a PATCH request. Therefore
        // manually strip them
        // @see \Drupal\Core\Entity\ContentEntityBase::__unset()
        // @see \Drupal\serialization\Normalizer\EntityNormalizer::normalize()
        // @see \Drupal\hal\Normalizer\ContentEntityNormalizer::normalize()
        $read_only_fields_so_far = array_slice($read_only_fields, 0, $i);
        $normalized = array_diff_key($normalized, array_flip($read_only_fields_so_far));
      }
      $serialized = $serializer->serialize($normalized, $format, $context);

      $this->httpRequest($url, 'PATCH', $serialized, $mime_type);
      $this->assertResponse(403);
      $this->assertResponseBody('{"error":"Access denied on updating field \'' . $field . '\'."}');

      if ($format === 'hal_json') {
        // We've just tried with this read-only field, now unset it.
        $entity->set($field, NULL);
      }
    }

    // Finally, with all read-only fields unset, the request should succeed.
    $normalized = $serializer->normalize($entity, $format, $context);
    if ($format !== 'hal_json') {
      $normalized = array_diff_key($normalized, array_combine($read_only_fields, $read_only_fields));
    }
    $serialized = $serializer->serialize($normalized, $format, $context);

    $this->httpRequest($url, 'PATCH', $serialized, $mime_type);
    $this->assertResponse(204);
    $this->assertResponseBody('');
  }

}