diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index 10874680a6..610bfd5db9 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -6,6 +6,7 @@ use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Cache\CacheableResponseInterface; use Drupal\Core\Config\Entity\ConfigEntityType; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Config\ConfigFactoryInterface; @@ -180,31 +181,26 @@ public function post(EntityInterface $entity = NULL) { // Validate the received data before saving. $this->validate($entity); - try { - $entity->save(); - $this->logger->notice('Created entity %type with ID %id.', ['%type' => $entity->getEntityTypeId(), '%id' => $entity->id()]); + $entity->save(); + $this->logger->notice('Created entity %type with ID %id.', ['%type' => $entity->getEntityTypeId(), '%id' => $entity->id()]); - // 201 Created responses return the newly created entity in the response - // body. These responses are not cacheable, so we add no cacheability - // metadata here. - $headers = []; - if (in_array('canonical', $entity->uriRelationships(), TRUE)) { - $url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE); - $headers['Location'] = $url->getGeneratedUrl(); - } - return new ModifiedResourceResponse($entity, 201, $headers); - } - catch (EntityStorageException $e) { - throw new HttpException(500, 'Internal Server Error', $e); + // 201 Created responses return the newly created entity in the response + // body. These responses are not cacheable, so we add no cacheability + // metadata here. + $headers = []; + if (in_array('canonical', $entity->uriRelationships(), TRUE)) { + $url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE); + $headers['Location'] = $url->getGeneratedUrl(); } + return new ModifiedResourceResponse($entity, 201, $headers); } /** * Responds to entity PATCH requests. * - * @param \Drupal\Core\Entity\EntityInterface $original_entity + * @param \Drupal\Core\Entity\ContentEntityInterface $original_entity * The original entity object. - * @param \Drupal\Core\Entity\EntityInterface $entity + * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity. * * @return \Drupal\rest\ModifiedResourceResponse @@ -212,7 +208,7 @@ public function post(EntityInterface $entity = NULL) { * * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ - public function patch(EntityInterface $original_entity, EntityInterface $entity = NULL) { + public function patch(ContentEntityInterface $original_entity, ContentEntityInterface $entity = NULL) { if ($entity == NULL) { throw new BadRequestHttpException('No entity content received.'); } @@ -227,41 +223,41 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity // Overwrite the received fields. foreach ($entity->_restSubmittedFields as $field_name) { - $field = $entity->get($field_name); - $original_field = $original_entity->get($field_name); + foreach ($entity->getTranslationLanguages() as $langcode => $language) { + if (!$original_entity->hasTranslation($langcode)) { + $original_entity->addTranslation($langcode, $original_entity->toArray()); + } + $field = $entity->getTranslation($langcode)->get($field_name); + $original_field = $original_entity->getTranslation($langcode)->get($field_name); - // If the user has access to view the field, we need to check update - // access regardless of the field value to avoid information disclosure. - // (Otherwise the user may try PATCHing with value after value, until they - // send the current value for the field, and then they won't get a 403 - // response anymore, which indicates that the value they sent in the PATCH - // request body matches the current value.) - if (!$original_field->access('view')) { - if (!$original_field->access('edit')) { + // If the user has access to view the field, we need to check update + // access regardless of the field value to avoid information disclosure. + // (Otherwise the user may try PATCHing with value after value, until they + // send the current value for the field, and then they won't get a 403 + // response anymore, which indicates that the value they sent in the PATCH + // request body matches the current value.) + if (!$original_field->access('view')) { + if (!$original_field->access('edit')) { + throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); + } + } + // Check access for all received fields, but only if they are being + // changed. The bundle of an entity, for example, must be provided for + // denormalization to succeed, but it may not be changed. + elseif (!$original_field->equals($field) && !$original_field->access('edit')) { throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); } + $original_entity->getTranslation($langcode)->set($field_name, $field->getValue()); } - // Check access for all received fields, but only if they are being - // changed. The bundle of an entity, for example, must be provided for - // denormalization to succeed, but it may not be changed. - elseif (!$original_field->equals($field) && !$original_field->access('edit')) { - throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); - } - $original_entity->set($field_name, $field->getValue()); } // Validate the received data before saving. $this->validate($original_entity); - try { - $original_entity->save(); - $this->logger->notice('Updated entity %type with ID %id.', ['%type' => $original_entity->getEntityTypeId(), '%id' => $original_entity->id()]); + $original_entity->save(); + $this->logger->notice('Updated entity %type with ID %id.', ['%type' => $original_entity->getEntityTypeId(), '%id' => $original_entity->id()]); - // Return the updated entity in the response body. - return new ModifiedResourceResponse($original_entity, 200); - } - catch (EntityStorageException $e) { - throw new HttpException(500, 'Internal Server Error', $e); - } + // Return the updated entity in the response body. + return new ModifiedResourceResponse($original_entity, 200); } /** diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php index 09b4b64bae..a769b6b6e3 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php @@ -3,7 +3,7 @@ namespace Drupal\rest\Plugin\rest\resource; use Drupal\Component\Render\PlainTextOutput; -use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; @@ -16,31 +16,31 @@ /** * Verifies that the whole entity does not violate any validation constraints. * - * @param \Drupal\Core\Entity\EntityInterface $entity + * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity to validate. * * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException * If validation errors are found. */ - protected function validate(EntityInterface $entity) { + protected function validate(ContentEntityInterface $entity) { // @todo Remove when https://www.drupal.org/node/2164373 is committed. if (!$entity instanceof FieldableEntityInterface) { return; } - $violations = $entity->validate(); - - // Remove violations of inaccessible fields as they cannot stem from our - // changes. - $violations->filterByFieldAccess(); - - if ($violations->count() > 0) { - $message = "Unprocessable Entity: validation failed.\n"; - foreach ($violations as $violation) { - // We strip every HTML from the error message to have a nicer to read - // message on REST responses. - $message .= $violation->getPropertyPath() . ': ' . PlainTextOutput::renderFromHtml($violation->getMessage()) . "\n"; + foreach ($entity->getTranslationLanguages() as $langcode => $language) { + $violations = $entity->getTranslation($langcode)->validate(); + // Remove violations of inaccessible fields as they cannot stem from our + // changes. + $violations->filterByFieldAccess(); + if ($violations->count() > 0) { + $message = "Unprocessable Entity: validation failed.\n"; + foreach ($violations as $violation) { + // We strip every HTML from the error message to have a nicer to read + // message on REST responses. + $message .= $violation->getPropertyPath() . ': ' . PlainTextOutput::renderFromHtml($violation->getMessage()) . "\n"; + } + throw new UnprocessableEntityHttpException($message); } - throw new UnprocessableEntityHttpException($message); } } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php index 5b5101445a..037bb3b606 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php @@ -277,8 +277,8 @@ public function testPostDxWithoutCriticalBaseFields() { $response = $this->request('POST', $url, $request_options); // @todo Uncomment, remove next 3 lines in https://www.drupal.org/node/2820364. $this->assertSame(500, $response->getStatusCode()); - $this->assertSame(['text/plain; charset=UTF-8'], $response->getHeader('Content-Type')); - $this->assertStringStartsWith('The website encountered an unexpected error. Please try again later.

Symfony\Component\HttpKernel\Exception\HttpException: Internal Server Error in Drupal\rest\Plugin\rest\resource\EntityResource->post()', (string) $response->getBody()); + $this->assertSame(['application/json'], $response->getHeader('Content-Type')); + $this->assertSame('{"message":"A fatal error occurred: The \u0022\u0022 entity type does not exist."}', (string) $response->getBody()); //$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nentity_type: This value should not be null.\n", $response); // DX: 422 when missing 'entity_id' field. diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 72589bd3b8..5753c85942 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -473,12 +473,16 @@ public function testGet() { if ($this->entity->getEntityType()->getLinkTemplates()) { $this->assertArrayHasKey('Link', $response->getHeaders()); $link_relation_type_manager = $this->container->get('plugin.manager.link_relation_type'); - $expected_link_relation_headers = array_map(function ($relation_name) use ($link_relation_type_manager) { + $expected_link_relation_headers = array_filter(array_map(function ($relation_name) use ($link_relation_type_manager) { + // @todo What to do about content_translation link relationships? + if (strpos($relation_name, 'drupal:content-translation') !== FALSE) { + return FALSE; + } $link_relation_type = $link_relation_type_manager->createInstance($relation_name); return $link_relation_type->isRegistered() ? $link_relation_type->getRegisteredName() : $link_relation_type->getExtensionUri(); - }, array_keys($this->entity->getEntityType()->getLinkTemplates())); + }, array_keys($this->entity->getEntityType()->getLinkTemplates()))); $parse_rel_from_link_header = function ($value) use ($link_relation_type_manager) { $matches = []; if (preg_match('/rel="([^"]+)"/', $value, $matches) === 1) {