diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonCookieTranslationsTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonCookieTranslationsTest.php
new file mode 100644
index 0000000..d67c2cd
--- /dev/null
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonCookieTranslationsTest.php
@@ -0,0 +1,198 @@
+grantPermissionsToTestedRole(['translate any entity', 'administer nodes']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function createEntity() {
+ if (!ConfigurableLanguage::load('de')) {
+ ConfigurableLanguage::createFromLangcode('de')->save();
+ }
+
+ /** @var \Drupal\node\NodeInterface $node */
+ $node = parent::createEntity();
+
+ ContentLanguageSettings::loadByEntityTypeBundle('node', 'camelids')
+ ->setLanguageAlterable(TRUE)
+ ->setDefaultLangcode('en')
+ ->save();
+
+ $translation = $node->addTranslation('de', $node->toArray());
+ $translation->get('title')->value = 'Lama';
+ $node->save();
+ return $node;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getExpectedNormalizedEntity() {
+ $normalization = parent::getExpectedNormalizedEntity();
+
+ foreach ($this->entity->getFieldDefinitions() as $field_name => $field_definition) {
+ // @todo It seems that currently two items exist if the *storage* is
+ // translatable, should be when the field is?
+ if ($field_definition->getFieldStorageDefinition()->isTranslatable() && isset($normalization[$field_name])) {
+ $normalization[$field_name][1] = [
+ 'lang' => 'de',
+ ] + $normalization[$field_name][0];
+ }
+ }
+
+ $normalization['title'][1]['value'] = 'Lama';
+ $normalization['langcode'][1]['value'] = 'de';
+ $normalization['default_langcode'][1]['value'] = '0';
+
+ $author = User::load($this->entity->getOwnerId());
+ $normalization['_links'][$this->baseUrl . '/rest/relation/node/camelids/uid'][1] = [
+ 'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json',
+ 'lang' => 'de',
+ ];
+ $normalization['_embedded'][$this->baseUrl . '/rest/relation/node/camelids/uid'][1] = [
+ '_links' => [
+ 'self' => [
+ 'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json',
+ ],
+ 'type' => [
+ 'href' => $this->baseUrl . '/rest/type/user/user',
+ ],
+ ],
+ 'uuid' => [
+ ['value' => $author->uuid()]
+ ],
+ 'lang' => 'de',
+ ];
+
+ return $normalization;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testPost() {
+ parent::testPost();
+
+ // Create an entity with a translation.
+ $normalized_entity = $this->getExpectedNormalizedEntity();
+ unset($normalized_entity['nid']);
+ unset($normalized_entity['vid']);
+ unset($normalized_entity['uuid']);
+ unset($normalized_entity['changed']);
+ unset($normalized_entity['revision_timestamp']);
+ unset($normalized_entity['_embedded'][$this->baseUrl . '/rest/relation/node/camelids/revision_uid']);
+ $entity_body_with_a_translations = $this->serializer->encode($normalized_entity, static::$format);
+
+ $request_options = [];
+ $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
+ $request_options[RequestOptions::BODY] = $entity_body_with_a_translations;
+ $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST'));
+
+ $url = $this->getEntityResourcePostUrl();
+ $url->setOption('query', ['_format' => static::$format]);
+
+ // 201 for well-formed request.
+ $response = $this->request('POST', $url, $request_options);
+ $this->assertResourceResponse(201, FALSE, $response);
+ /** @var \Drupal\node\NodeInterface $created_node */
+ $created_node = $this->serializer->deserialize((string) $response->getBody(), get_class($this->entity), static::$format);
+ $this->assertSame([$created_node->toUrl('canonical')->setAbsolute(TRUE)->toString()], $response->getHeader('Location'));
+ $this->assertFalse($response->hasHeader('X-Drupal-Cache'));
+
+ $this->assertSame('Llama', $created_node->getTitle());
+ $this->assertSame('Lama', $created_node->getTranslation('de')->getTitle());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testPatch() {
+ parent::testPatch();
+
+ $title_with_translation = $this->getNormalizedPatchEntity();
+ $title_with_translation['title'][0]['lang'] = 'en';
+ $title_with_translation['title'][1] = [
+ 'value' => 'Drama-Lama',
+ 'lang' => 'de',
+ ];
+
+ $entity_body_title_with_translation = $this->serializer->encode($title_with_translation, static::$format);
+
+ $request_options = [];
+ $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
+ $request_options[RequestOptions::BODY] = $entity_body_title_with_translation;
+ $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST'));
+
+ $url = $this->getUrl();
+ $url->setOption('query', ['_format' => static::$format]);
+
+ $response = $this->request('PATCH', $url, $request_options);
+ $this->assertResourceResponse(200, FALSE, $response);
+ $this->assertFalse($response->hasHeader('X-Drupal-Cache'));
+
+ $storage = \Drupal::entityTypeManager()->getStorage('node');
+ $storage->resetCache();
+
+ $patched_node = $storage->load($this->entity->id());
+ $this->assertSame('Dramallama', $patched_node->getTitle());
+ $this->assertSame('Drama-Lama', $patched_node->getTranslation('de')->getTitle());
+ }
+
+}
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 1087468..610bfd5 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()]);
-
- // 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);
+ $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);
}
/**
* 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);
-
- // 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')) {
+ 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')) {
+ 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 09b4b64..a769b6b 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 5b51014..f3bd70f 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
@@ -278,7 +278,7 @@ public function testPostDxWithoutCriticalBaseFields() {
// @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->assertStringStartsWith('The website encountered an unexpected error. Please try again later.Drupal\Core\Entity\EntityStorageException: The "" entity type does not exist. in Drupal\Core\Entity\Sql\SqlContentEntityStorage->save()', (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 72589bd..5753c85 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) {
diff --git a/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php b/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php
index 8c9758e..4683277 100644
--- a/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php
+++ b/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php
@@ -3,6 +3,7 @@
namespace Drupal\Tests\rest\Unit;
use Drupal\Core\Entity\EntityConstraintViolationList;
+use Drupal\Core\Language\LanguageInterface;
use Drupal\node\Entity\Node;
use Drupal\Tests\UnitTestCase;
use Drupal\user\Entity\User;
@@ -28,8 +29,12 @@ public function testValidate() {
$violations->filterByFieldAccess()->shouldBeCalled()->willReturn([]);
$violations->count()->shouldBeCalled()->willReturn(0);
+ $translatable_entity = $this->prophesize(Node::class);
+ $translatable_entity->validate()->shouldBeCalled()->willReturn($violations->reveal());
+
$entity = $this->prophesize(Node::class);
- $entity->validate()->shouldBeCalled()->willReturn($violations->reveal());
+ $entity->getTranslationLanguages()->shouldBeCalled()->willReturn(['en' => LanguageInterface::class]);
+ $entity->getTranslation('en')->shouldBeCalled()->willReturn($translatable_entity);
$method->invoke($trait, $entity->reveal());
}
@@ -57,7 +62,11 @@ public function testFailedValidate() {
->method('filterByFieldAccess')
->will($this->returnValue([]));
- $entity->validate()->willReturn($violations);
+ $translatable_entity = $this->prophesize(Node::class);
+ $translatable_entity->validate()->shouldBeCalled()->willReturn($violations);
+
+ $entity->getTranslationLanguages()->shouldBeCalled()->willReturn(['en' => LanguageInterface::class]);
+ $entity->getTranslation('en')->shouldBeCalled()->willReturn($translatable_entity);
$trait = $this->getMockForTrait('Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait');