diff --git a/core/modules/hal/hal.services.yml b/core/modules/hal/hal.services.yml
index 97fcbfb13dcd4ddcf794684d91466d7c99af5a45..b2c898fc5608c809f76046b269cc8cd82e7894fd 100644
--- a/core/modules/hal/hal.services.yml
+++ b/core/modules/hal/hal.services.yml
@@ -17,6 +17,11 @@ services:
tags:
- { name: normalizer, priority: 20 }
arguments: ['@entity.manager', '@http_client', '@hal.link_manager', '@module_handler']
+ serializer.normalizer.timestamp_item.hal:
+ class: Drupal\hal\Normalizer\TimestampItemNormalizer
+ tags:
+ # Priority must be higher than serializer.normalizer.field_item.hal.
+ - { name: normalizer, priority: 20, bc: bc_timestamp_normalizer_unix, bc_config_name: 'serialization.settings' }
serializer.normalizer.entity.hal:
class: Drupal\hal\Normalizer\ContentEntityNormalizer
arguments: ['@hal.link_manager', '@entity.manager', '@module_handler']
diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
index dc5aec98994ada8055135dff94b6680b0ed271ea..0c2ee9ed4d9a7c69b1bf3c9b7b7bb8a26d9a9d7e 100644
--- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
@@ -21,25 +21,13 @@ class FieldItemNormalizer extends NormalizerBase {
* {@inheritdoc}
*/
public function normalize($field_item, $format = NULL, array $context = []) {
- $values = [];
- // We normalize each individual property, so each can do their own casting,
- // if needed.
- /** @var \Drupal\Core\TypedData\TypedDataInterface $property */
- foreach ($field_item as $property_name => $property) {
- $values[$property_name] = $this->serializer->normalize($property, $format, $context);
- }
-
- if (isset($context['langcode'])) {
- $values['lang'] = $context['langcode'];
- }
-
// The values are wrapped in an array, and then wrapped in another array
// keyed by field name so that field items can be merged by the
// FieldNormalizer. This is necessary for the EntityReferenceItemNormalizer
// to be able to place values in the '_links' array.
$field = $field_item->getParent();
return [
- $field->getName() => [$values],
+ $field->getName() => [$this->normalizedFieldValues($field_item, $format, $context)],
];
}
@@ -85,6 +73,35 @@ protected function constructValue($data, $context) {
return $data;
}
+ /**
+ * Normalizes field values for an item.
+ *
+ * @param \Drupal\Core\Field\FieldItemInterface $field_item
+ * The field item instance.
+ * @param string|null $format
+ * The normalization format.
+ * @param array $context
+ * The context passed into the normalizer.
+ *
+ * @return array
+ * An array of field item values, keyed by property name.
+ */
+ protected function normalizedFieldValues(FieldItemInterface $field_item, $format, array $context) {
+ $denormalized = [];
+ // We normalize each individual property, so each can do their own casting,
+ // if needed.
+ /** @var \Drupal\Core\TypedData\TypedDataInterface $property */
+ foreach ($field_item as $property_name => $property) {
+ $denormalized[$property_name] = $this->serializer->normalize($property, $format, $context);
+ }
+
+ if (isset($context['langcode'])) {
+ $denormalized['lang'] = $context['langcode'];
+ }
+
+ return $denormalized;
+ }
+
/**
* Get a translated version of the field item instance.
*
diff --git a/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php b/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..c389e793501dd1c7fdce32c8e6be65dcba10a826
--- /dev/null
+++ b/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php
@@ -0,0 +1,31 @@
+processNormalizedValues($normalized);
+ }
+
+}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
index d4ed47c37c1ba58366f6bff66575e7d06da59b96..aeac85ab61f4e2e1e2d50a46b866585cf4bf8565 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
@@ -34,11 +34,11 @@ class NodeHalJsonAnonTest extends NodeResourceTestBase {
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
+ 'revision_timestamp',
'created',
'changed',
'promote',
'sticky',
- 'revision_timestamp',
'revision_uid',
];
diff --git a/core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php b/core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php
index 5b752f7bb6ab7597ed76dccdd24c09068f27fec6..2597af4affc13bd1e1ab077573a287a8e7c14c06 100644
--- a/core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php
+++ b/core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php
@@ -43,7 +43,7 @@ public function testNodeTranslation() {
$node = Node::create([
'title' => $this->randomMachineName(),
- 'uid' => $user->id(),
+ 'uid' => (int) $user->id(),
'type' => $node_type->id(),
'status' => NodeInterface::PUBLISHED,
'langcode' => 'en',
diff --git a/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php b/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..a275f646f7426a519506cbc458f83b8408f772da
--- /dev/null
+++ b/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php
@@ -0,0 +1,43 @@
+config('serialization.settings')->get('bc_timestamp_normalizer_unix')) {
+ return ['value' => $timestamp];
+ }
+
+ // Otherwise, format the date string to the same that
+ // \Drupal\serialization\Normalizer\TimestampItemNormalizer will produce.
+ $date = new \DateTime();
+ $date->setTimestamp($timestamp);
+ $date->setTimezone(new \DateTimeZone('UTC'));
+
+ // Format is also added to the expected return values.
+ return [
+ 'value' => $date->format(\DateTime::RFC3339),
+ 'format' => \DateTime::RFC3339,
+ ];
+ }
+
+}
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 961441213af3008e75bab3557649873362eaa96e..7b65c848b7830337f7c294a7c385448e4901eff6 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
@@ -6,13 +6,14 @@
use Drupal\comment\Entity\CommentType;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\entity_test\Entity\EntityTest;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
use GuzzleHttp\RequestOptions;
abstract class CommentResourceTestBase extends EntityResourceTestBase {
- use CommentTestTrait;
+ use CommentTestTrait, BcTimestampNormalizerUnixTestTrait;
/**
* {@inheritdoc}
@@ -148,14 +149,10 @@ protected function getExpectedNormalizedEntity() {
],
],
'created' => [
- [
- 'value' => 123456789,
- ],
+ $this->formatExpectedTimestampItemValues(123456789),
],
'changed' => [
- [
- 'value' => $this->entity->getChangedTime(),
- ],
+ $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
],
'default_langcode' => [
[
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 6e34de2bf56ebfe759c9445e3f3cd1ef828b3a51..277c47042be4f36fbd1d92bcd1d99a1afa46be30 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -465,9 +465,9 @@ public function testGet() {
// for the keys with the array order the same (it needs to match with
// identical comparison).
$expected = $this->getExpectedNormalizedEntity();
- ksort($expected);
+ static::recursiveKSort($expected);
$actual = $this->serializer->decode((string) $response->getBody(), static::$format);
- ksort($actual);
+ static::recursiveKSort($actual);
$this->assertSame($expected, $actual);
// Not only assert the normalization, also assert deserialization of the
@@ -507,12 +507,14 @@ public function testGet() {
}
$this->assertSame($get_headers, $head_headers);
+ // BC: serialization_update_8302().
// Only run this for fieldable entities. It doesn't make sense for config
// entities as config values are already casted. They also run through the
// ConfigEntityNormalizer, which doesn't deal with fields individually.
if ($this->entity instanceof FieldableEntityInterface) {
+ // Test primitive data casting BC (strings).
$this->config('serialization.settings')->set('bc_primitives_as_strings', TRUE)->save(TRUE);
- // Rebuild the container so new config is reflected in the removal of the
+ // Rebuild the container so new config is reflected in the addition of the
// PrimitiveDataNormalizer.
$this->rebuildAll();
@@ -528,10 +530,48 @@ public function testGet() {
// Config entities are not affected.
// @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize()
$expected = static::castToString($expected);
- ksort($expected);
+ static::recursiveKSort($expected);
$actual = $this->serializer->decode((string) $response->getBody(), static::$format);
- ksort($actual);
+ static::recursiveKSort($actual);
$this->assertSame($expected, $actual);
+
+ // Reset the config value and rebuild.
+ $this->config('serialization.settings')->set('bc_primitives_as_strings', FALSE)->save(TRUE);
+ $this->rebuildAll();
+ }
+
+ // BC: serialization_update_8401().
+ // Only run this for fieldable entities. It doesn't make sense for config
+ // entities as config values always use the raw values (as per the config
+ // schema), returned directly from the ConfigEntityNormalizer, which
+ // doesn't deal with fields individually.
+ if ($this->entity instanceof FieldableEntityInterface) {
+ // Test the BC settings for timestamp values.
+ $this->config('serialization.settings')->set('bc_timestamp_normalizer_unix', TRUE)->save(TRUE);
+ // Rebuild the container so new config is reflected in the addition of the
+ // TimestampItemNormalizer.
+ $this->rebuildAll();
+
+
+ $response = $this->request('GET', $url, $request_options);
+ $this->assertResourceResponse(200, FALSE, $response);
+
+
+ // This ensures the BC layer for bc_timestamp_normalizer_unix works as
+ // expected. This method should be using
+ // ::formatExpectedTimestampValue() to generate the timestamp value. This
+ // will take into account the above config setting.
+ $expected = $this->getExpectedNormalizedEntity();
+ // Config entities are not affected.
+ // @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize()
+ static::recursiveKSort($expected);
+ $actual = $this->serializer->decode((string) $response->getBody(), static::$format);
+ static::recursiveKSort($actual);
+ $this->assertSame($expected, $actual);
+
+ // Reset the config value and rebuild.
+ $this->config('serialization.settings')->set('bc_timestamp_normalizer_unix', FALSE)->save(TRUE);
+ $this->rebuildAll();
}
@@ -634,6 +674,27 @@ protected static function castToString(array $normalization) {
return $normalization;
}
+ /**
+ * Recursively sorts an array by key.
+ *
+ * @param array $array
+ * An array to sort.
+ *
+ * @return array
+ * The sorted array.
+ */
+ protected static function recursiveKSort(array &$array) {
+ // First, sort the main array.
+ ksort($array);
+
+ // Then check for child arrays.
+ foreach ($array as $key => &$value) {
+ if (is_array($value)) {
+ static::recursiveKSort($value);
+ }
+ }
+ }
+
/**
* Tests a POST request for an entity, plus edge cases to ensure good DX.
*/
@@ -968,15 +1029,19 @@ public function testPatch() {
// DX: 403 when sending PATCH request with read-only fields.
- foreach (static::$patchProtectedFieldNames as $field_name) {
- $normalization = $this->getNormalizedPatchEntity() + [$field_name => [['value' => $this->randomString()]]];
- $request_options[RequestOptions::BODY] = $this->serializer->serialize($normalization, static::$format);
+ // First send all fields (the "maximum normalization"). Assert the expected
+ // error message for the first PATCH-protected field. Remove that field from
+ // the normalization, send another request, assert the next PATCH-protected
+ // field error message. And so on.
+ $max_normalization = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format);
+ for ($i = 0; $i < count(static::$patchProtectedFieldNames); $i++) {
+ $max_normalization = $this->removeFieldsFromNormalization($max_normalization, array_slice(static::$patchProtectedFieldNames, 0, $i));
+ $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format);
$response = $this->request('PATCH', $url, $request_options);
- $this->assertResourceErrorResponse(403, "Access denied on updating field '$field_name'.", $response);
+ $this->assertResourceErrorResponse(403, "Access denied on updating field '" . static::$patchProtectedFieldNames[$i] . "'.", $response);
}
// 200 for well-formed request that sends the maximum number of fields.
- $max_normalization = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format);
$max_normalization = $this->removeFieldsFromNormalization($max_normalization, static::$patchProtectedFieldNames);
$request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format);
$response = $this->request('PATCH', $url, $request_options);
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
index 2935a00fa72faac6010c9fe07ced155a54559e8a..204c7ff378e6361b45eba5f18084504055a26997 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
@@ -3,11 +3,14 @@
namespace Drupal\Tests\rest\Functional\EntityResource\EntityTest;
use Drupal\entity_test\Entity\EntityTest;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
abstract class EntityTestResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -92,9 +95,7 @@ protected function getExpectedNormalizedEntity() {
]
],
'created' => [
- [
- 'value' => (int) $this->entity->get('created')->value,
- ]
+ $this->formatExpectedTimestampItemValues((int) $this->entity->get('created')->value)
],
'user_id' => [
[
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTestLabel/EntityTestLabelResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTestLabel/EntityTestLabelResourceTestBase.php
index d0307872f9691c555e5bc68c0900578d21355c14..26d0eca9de83567e9b2a96d259e301c21a24a265 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTestLabel/EntityTestLabelResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTestLabel/EntityTestLabelResourceTestBase.php
@@ -3,11 +3,14 @@
namespace Drupal\Tests\rest\Functional\EntityResource\EntityTestLabel;
use Drupal\entity_test\Entity\EntityTestLabel;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
abstract class EntityTestLabelResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -94,9 +97,7 @@ protected function getExpectedNormalizedEntity() {
],
],
'created' => [
- [
- 'value' => (int) $this->entity->get('created')->value,
- ],
+ $this->formatExpectedTimestampItemValues((int) $this->entity->get('created')->value),
],
'user_id' => [
[
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Feed/FeedResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Feed/FeedResourceTestBase.php
index f15a5e0554403c6cb524a616c54ed3fb5fb4f0bc..1e79395a605f5f6f905888c9fc2f1e91fe406ec0 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Feed/FeedResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Feed/FeedResourceTestBase.php
@@ -2,11 +2,14 @@
namespace Drupal\Tests\rest\Functional\EntityResource\Feed;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase;
use Drupal\aggregator\Entity\Feed;
abstract class FeedResourceTestBase extends EntityTestResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -92,14 +95,10 @@ protected function getExpectedNormalizedEntity() {
]
],
'checked' => [
- [
- 'value' => 123456789
- ]
+ $this->formatExpectedTimestampItemValues(123456789),
],
'queued' => [
- [
- 'value' => 123456789
- ]
+ $this->formatExpectedTimestampItemValues(123456789),
],
'link' => [
[
@@ -127,9 +126,7 @@ protected function getExpectedNormalizedEntity() {
]
],
'modified' => [
- [
- 'value' => 123456789
- ]
+ $this->formatExpectedTimestampItemValues(123456789),
],
];
}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Item/ItemResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Item/ItemResourceTestBase.php
index 9b72b66142e280af6d5795e5def31cdea655612d..f217b97fbac0f14da4509308895adf0ef2bff058 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Item/ItemResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Item/ItemResourceTestBase.php
@@ -4,6 +4,7 @@
use Drupal\aggregator\Entity\Feed;
use Drupal\aggregator\Entity\Item;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
@@ -11,6 +12,8 @@
*/
abstract class ItemResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -113,9 +116,7 @@ protected function getExpectedNormalizedEntity() {
'author' => [],
'description' => [],
'timestamp' => [
- [
- 'value' => 123456789,
- ],
+ $this->formatExpectedTimestampItemValues(123456789),
],
'guid' => [],
];
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php
index f907d9a5983862b3e57862d601d96f17dad3cb92..0b1f967387e1b5e920ee061b34de3a2e4c1cef62 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php
@@ -3,6 +3,7 @@
namespace Drupal\Tests\rest\Functional\EntityResource\MenuLinkContent;
use Drupal\menu_link_content\Entity\MenuLinkContent;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
@@ -10,6 +11,8 @@
*/
abstract class MenuLinkContentResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -161,9 +164,7 @@ protected function getExpectedNormalizedEntity() {
],
],
'changed' => [
- [
- 'value' => $this->entity->getChangedTime(),
- ],
+ $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
],
'default_langcode' => [
[
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
index 92fad1e39d020d4eb52c819f8c17cc8cb3340e63..53367b8fe07997bd42dadefe9a08001fd02c8871 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
@@ -4,11 +4,14 @@
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
abstract class NodeResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -23,12 +26,12 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase {
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
+ 'revision_timestamp',
+ 'revision_uid',
'created',
'changed',
'promote',
'sticky',
- 'revision_timestamp',
- 'revision_uid',
];
/**
@@ -119,14 +122,10 @@ protected function getExpectedNormalizedEntity() {
],
],
'created' => [
- [
- 'value' => 123456789,
- ],
+ $this->formatExpectedTimestampItemValues(123456789),
],
'changed' => [
- [
- 'value' => $this->entity->getChangedTime(),
- ],
+ $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
],
'promote' => [
[
@@ -139,9 +138,7 @@ protected function getExpectedNormalizedEntity() {
],
],
'revision_timestamp' => [
- [
- 'value' => 123456789,
- ],
+ $this->formatExpectedTimestampItemValues(123456789),
],
'revision_translation_affected' => [
[
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
index 698451b58cf353948c7c7f0ed9dd628a11e2bad7..7dfe46e08d690cab47d50e79178c9a06a45197ff 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
@@ -4,10 +4,13 @@
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
abstract class TermResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -107,9 +110,7 @@ protected function getExpectedNormalizedEntity() {
],
],
'changed' => [
- [
- 'value' => $this->entity->getChangedTime(),
- ],
+ $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
],
'default_langcode' => [
[
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
index 0195ddaeb8174af275fadf40630b2c3c4b72dc78..25dcf75d23040051881edf0395ace37179772034 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
@@ -3,12 +3,15 @@
namespace Drupal\Tests\rest\Functional\EntityResource\User;
use Drupal\Core\Url;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
use GuzzleHttp\RequestOptions;
abstract class UserResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -98,14 +101,10 @@ protected function getExpectedNormalizedEntity() {
],
],
'created' => [
- [
- 'value' => 123456789,
- ],
+ $this->formatExpectedTimestampItemValues(123456789),
],
'changed' => [
- [
- 'value' => $this->entity->getChangedTime(),
- ],
+ $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
],
'default_langcode' => [
[
diff --git a/core/modules/serialization/config/install/serialization.settings.yml b/core/modules/serialization/config/install/serialization.settings.yml
index 39c583aaa63141944928e33c0b22feeed7f5f531..a2758c2d021f6b8735dfa580cbea5c9b4d20695f 100644
--- a/core/modules/serialization/config/install/serialization.settings.yml
+++ b/core/modules/serialization/config/install/serialization.settings.yml
@@ -2,3 +2,10 @@
# this was usually returned from database storage. A primitive data normalizer
# has been introduced to get the casted value instead.
bc_primitives_as_strings: false
+# Before Drupal 8.3, timestamps were always returned as Unix timestamps, which
+# are not a universal format for interchange. Now, RFC3339 timestamps are
+# returned. New Drupal installations opt out from this by default (hence this
+# is set to false), existing installations opt in to it.
+# @see serialization_update_8301()
+# @see https://www.drupal.org/node/2768651
+bc_timestamp_normalizer_unix: false
diff --git a/core/modules/serialization/config/schema/serialization.schema.yml b/core/modules/serialization/config/schema/serialization.schema.yml
index 0fdd2da14dba6bf955f80ae7112c6b58b816005c..1d44b3dcec5fcf8095a185f2208ae4aee6e7986a 100644
--- a/core/modules/serialization/config/schema/serialization.schema.yml
+++ b/core/modules/serialization/config/schema/serialization.schema.yml
@@ -5,3 +5,6 @@ serialization.settings:
bc_primitives_as_strings:
type: boolean
label: 'Whether to retain pre Drupal 8.3 behavior of serializing all primitive items as strings.'
+ bc_timestamp_normalizer_unix:
+ type: boolean
+ label: 'Whether the pre Drupal 8.4 behavior of returning Unix timestamps instead of RFC3339 timestamps for TimestampItem fields is enabled or not.'
diff --git a/core/modules/serialization/serialization.install b/core/modules/serialization/serialization.install
index 6620b446efc7626a912acd389d5d01d7a92de318..afc3311b4ce633b767bccafd04dcdb13f4999354 100644
--- a/core/modules/serialization/serialization.install
+++ b/core/modules/serialization/serialization.install
@@ -46,3 +46,12 @@ function serialization_update_8302() {
return t('The REST API will no longer output all values as strings. Integers/booleans will be used where appropriate. If your site depends on these value being strings, read the change record to learn how to enable the BC mode.');
}
+
+/**
+ * Enable BC for timestamp formatting: continue to return UNIX timestamps.
+ */
+function serialization_update_8401() {
+ $config_factory = \Drupal::configFactory();
+ $serialization_settings = $config_factory->getEditable('serialization.settings');
+ $serialization_settings->set('bc_timestamp_normalizer_unix', TRUE)->save(TRUE);
+}
diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml
index 3607182ee2590603da9ddfc5f4ce17e1dada2e2a..dca60947870859388228ad50dd2049ba43b6b33f 100644
--- a/core/modules/serialization/serialization.services.yml
+++ b/core/modules/serialization/serialization.services.yml
@@ -52,6 +52,12 @@ services:
# Priority must be higher than serialization.normalizer.field but less
# than hal field normalizer.
- { name: normalizer, priority: 9 }
+ serializer.normalizer.timestamp_item:
+ class: Drupal\serialization\Normalizer\TimestampItemNormalizer
+ tags:
+ # Priority must be higher than serializer.normalizer.field_item and lower
+ # than hal normalizers.
+ - { name: normalizer, priority: 8, bc: bc_timestamp_normalizer_unix, bc_config_name: 'serialization.settings' }
serializer.normalizer.password_field_item:
class: Drupal\serialization\Normalizer\NullNormalizer
arguments: ['Drupal\Core\Field\Plugin\Field\FieldType\PasswordItem']
diff --git a/core/modules/serialization/src/EventSubscriber/BcConfigSubscriber.php b/core/modules/serialization/src/EventSubscriber/BcConfigSubscriber.php
index 79d3a905b07b9c09e719701b28f9389cf1aff3fc..4cb794ffd2bc59ef516013b91832dbf0caf2e3da 100644
--- a/core/modules/serialization/src/EventSubscriber/BcConfigSubscriber.php
+++ b/core/modules/serialization/src/EventSubscriber/BcConfigSubscriber.php
@@ -46,7 +46,7 @@ public function onConfigSave(ConfigCrudEvent $event) {
$saved_config = $event->getConfig();
if ($saved_config->getName() === 'serialization.settings') {
- if ($event->isChanged('bc_primitives_as_strings')) {
+ if ($event->isChanged('bc_primitives_as_strings') || $event->isChanged('bc_timestamp_normalizer_unix')) {
$this->kernel->invalidateContainer();
}
}
diff --git a/core/modules/serialization/src/Normalizer/EntityNormalizer.php b/core/modules/serialization/src/Normalizer/EntityNormalizer.php
index 84baec94be87fbfdb16780ee33f34244ba0e9613..4103d1dd7a5038fccc119fd7b644c28e5318e761 100644
--- a/core/modules/serialization/src/Normalizer/EntityNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/EntityNormalizer.php
@@ -40,13 +40,20 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
// The bundle property will be required to denormalize a bundleable
// fieldable entity.
- if ($entity_type_definition->hasKey('bundle') && $entity_type_definition->isSubclassOf(FieldableEntityInterface::class)) {
- // Get an array containing the bundle only. This also remove the bundle
- // key from the $data array.
- $bundle_data = $this->extractBundleData($data, $entity_type_definition);
+ if ($entity_type_definition->isSubclassOf(FieldableEntityInterface::class)) {
+ // Extract bundle data to pass into entity creation if the entity type uses
+ // bundles.
+ if ($entity_type_definition->hasKey('bundle')) {
+ // Get an array containing the bundle only. This also remove the bundle
+ // key from the $data array.
+ $create_params = $this->extractBundleData($data, $entity_type_definition);
+ }
+ else {
+ $create_params = [];
+ }
// Create the entity from bundle data only, then apply field values after.
- $entity = $this->entityManager->getStorage($entity_type_id)->create($bundle_data);
+ $entity = $this->entityManager->getStorage($entity_type_id)->create($create_params);
$this->denormalizeFieldData($data, $entity, $format, $context);
}
diff --git a/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php b/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..78f6030e57106772632a5fc7f5eef0ffefa4bdcc
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php
@@ -0,0 +1,87 @@
+ 'U',
+ 'ISO 8601' => \DateTime::ISO8601,
+ 'RFC 3339' => \DateTime::RFC3339,
+ ];
+
+ /**
+ * Processes normalized timestamp values to add a formatted date and format.
+ *
+ * @param array $normalized
+ * The normalized field data to process.
+ * @return array
+ * The processed data.
+ */
+ protected function processNormalizedValues(array $normalized) {
+ // Use a RFC 3339 timestamp with the time zone set to UTC to replace the
+ // timestamp value.
+ $date = new \DateTime();
+ $date->setTimestamp($normalized['value']);
+ $date->setTimezone(new \DateTimeZone('UTC'));
+ $normalized['value'] = $date->format(\DateTime::RFC3339);
+ // 'format' is not a property on TimestampItem fields. This is present to
+ // assist consumers of this data.
+ $normalized['format'] = \DateTime::RFC3339;
+
+ return $normalized;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function constructValue($data, $context) {
+ // Loop through the allowed formats and create a TimestampItem from the
+ // input data if it matches the defined pattern. Since the formats are
+ // unambiguous (i.e., they reference an absolute time with a defined time
+ // zone), only one will ever match.
+ $timezone = new \DateTimeZone('UTC');
+
+ // First check for a provided format.
+ if (!empty($data['format']) && in_array($data['format'], $this->allowedFormats)) {
+ $date = \DateTime::createFromFormat($data['format'], $data['value'], $timezone);
+ return ['value' => $date->getTimestamp()];
+ }
+ // Otherwise, loop through formats.
+ else {
+ foreach ($this->allowedFormats as $format) {
+ if (($date = \DateTime::createFromFormat($format, $data['value'], $timezone)) !== FALSE) {
+ return ['value' => $date->getTimestamp()];
+ }
+ }
+ }
+
+ $format_strings = [];
+
+ foreach ($this->allowedFormats as $label => $format) {
+ $format_strings[] = "\"$format\" ($label)";
+ }
+
+ $formats = implode(', ', $format_strings);
+ throw new UnexpectedValueException(sprintf('The specified date "%s" is not in an accepted format: %s.', $data['value'], $formats));
+ }
+
+}
diff --git a/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php b/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..704b22f784659e3236b24039f0f691ae542b6f6b
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php
@@ -0,0 +1,42 @@
+processNormalizedValues($data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function denormalize($data, $class, $format = NULL, array $context = []) {
+ if (empty($data['value'])) {
+ throw new InvalidArgumentException('No "value" attribute present');
+ }
+
+ return parent::denormalize($data, $class, $format, $context);
+ }
+
+}
diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
index 71fa6b3ec584d83f3c46b67de1bc15aa961c44bd..ebd4e0331789370572cee7a9403033e26aa19926 100644
--- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
+++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
@@ -4,6 +4,7 @@
use Drupal\Component\Utility\SafeMarkup;
use Drupal\entity_test\Entity\EntityTestMulRev;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
/**
* Tests that entities can be serialized to supported core formats.
@@ -12,6 +13,8 @@
*/
class EntitySerializationTest extends NormalizerTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* Modules to install.
*
@@ -106,7 +109,7 @@ public function testNormalize() {
['value' => 'entity_test_mulrev'],
],
'created' => [
- ['value' => $this->entity->created->value],
+ $this->formatExpectedTimestampItemValues($this->entity->created->value),
],
'user_id' => [
[
@@ -182,13 +185,15 @@ public function testSerialize() {
// Generate the expected xml in a way that allows changes to entity property
// order.
+ $expected_created = $this->formatExpectedTimestampItemValues($this->entity->created->value);
+
$expected = [
'id' => '' . $this->entity->id() . '',
'uuid' => '' . $this->entity->uuid() . '',
'langcode' => 'en',
'name' => '' . $this->values['name'] . '',
'type' => 'entity_test_mulrev',
- 'created' => '' . $this->entity->created->value . '',
+ 'created' => '' . $expected_created['value'] . '' . $expected_created['format'] . '',
'user_id' => '' . $this->user->id() . '' . $this->user->getEntityTypeId() . '' . $this->user->uuid() . '' . $this->user->url() . '',
'revision_id' => '' . $this->entity->getRevisionId() . '',
'default_langcode' => '1',
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php
index bbddbdd4844d8834efcb5d40b7233eb56b580c23..689d654bd9e9d4e82ec52f72259c25a11417b5dc 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php
@@ -302,6 +302,10 @@ public function testDenormalizeWithNoBundle() {
];
$entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
+ $entity_type->expects($this->once())
+ ->method('isSubClassOf')
+ ->with(FieldableEntityInterface::class)
+ ->willReturn(TRUE);
$entity_type->expects($this->once())
->method('hasKey')
->with('bundle')
@@ -314,6 +318,76 @@ public function testDenormalizeWithNoBundle() {
->with('test')
->will($this->returnValue($entity_type));
+ $key_1 = $this->getMock(FieldItemListInterface::class);
+ $key_2 = $this->getMock(FieldItemListInterface::class);
+
+ $entity = $this->getMock(FieldableEntityInterface::class);
+ $entity->expects($this->at(0))
+ ->method('get')
+ ->with('key_1')
+ ->willReturn($key_1);
+ $entity->expects($this->at(1))
+ ->method('get')
+ ->with('key_2')
+ ->willReturn($key_2);
+
+ $storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
+ $storage->expects($this->once())
+ ->method('create')
+ ->with([])
+ ->will($this->returnValue($entity));
+
+ $this->entityManager->expects($this->once())
+ ->method('getStorage')
+ ->with('test')
+ ->will($this->returnValue($storage));
+
+ $this->entityManager->expects($this->never())
+ ->method('getBaseFieldDefinitions');
+
+ // Setup expectations for the serializer. This will be called for each field
+ // item.
+ $serializer = $this->getMockBuilder('Symfony\Component\Serializer\Serializer')
+ ->disableOriginalConstructor()
+ ->setMethods(['denormalize'])
+ ->getMock();
+ $serializer->expects($this->at(0))
+ ->method('denormalize')
+ ->with('value_1', get_class($key_1), NULL, ['target_instance' => $key_1, 'entity_type' => 'test']);
+ $serializer->expects($this->at(1))
+ ->method('denormalize')
+ ->with('value_2', get_class($key_2), NULL, ['target_instance' => $key_2, 'entity_type' => 'test']);
+
+ $this->entityNormalizer->setSerializer($serializer);
+
+ $this->assertNotNull($this->entityNormalizer->denormalize($test_data, 'Drupal\Core\Entity\ContentEntityBase', NULL, ['entity_type' => 'test']));
+ }
+
+ /**
+ * Tests the denormalize method with no bundle defined.
+ *
+ * @covers ::denormalize
+ */
+ public function testDenormalizeWithNoFieldableEntityType() {
+ $test_data = [
+ 'key_1' => 'value_1',
+ 'key_2' => 'value_2',
+ ];
+
+ $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
+ $entity_type->expects($this->once())
+ ->method('isSubClassOf')
+ ->with(FieldableEntityInterface::class)
+ ->willReturn(FALSE);
+
+ $entity_type->expects($this->never())
+ ->method('getKey');
+
+ $this->entityManager->expects($this->once())
+ ->method('getDefinition')
+ ->with('test')
+ ->will($this->returnValue($entity_type));
+
$storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
$storage->expects($this->once())
->method('create')
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..fd1fc9ce9f3b773df2efe384864202ed1af7fbfd
--- /dev/null
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
@@ -0,0 +1,156 @@
+normalizer = new TimestampItemNormalizer();
+ }
+
+ /**
+ * @covers ::supportsNormalization
+ */
+ public function testSupportsNormalization() {
+ $timestamp_item = $this->createTimestampItemProphecy();
+ $this->assertTrue($this->normalizer->supportsNormalization($timestamp_item->reveal()));
+
+ $entity_ref_item = $this->prophesize(EntityReferenceItem::class);
+ $this->assertFalse($this->normalizer->supportsNormalization($entity_ref_item->reveal()));
+ }
+
+ /**
+ * @covers ::supportsDenormalization
+ */
+ public function testSupportsDenormalization() {
+ $timestamp_item = $this->createTimestampItemProphecy();
+ $this->assertTrue($this->normalizer->supportsDenormalization($timestamp_item->reveal(), TimestampItem::class));
+
+ // CreatedItem extends regular TimestampItem.
+ $timestamp_item = $this->prophesize(CreatedItem::class);
+ $this->assertTrue($this->normalizer->supportsDenormalization($timestamp_item->reveal(), TimestampItem::class));
+
+ $entity_ref_item = $this->prophesize(EntityReferenceItem::class);
+ $this->assertFalse($this->normalizer->supportsNormalization($entity_ref_item->reveal(), TimestampItem::class));
+ }
+
+ /**
+ * Tests the normalize function.
+ *
+ * @covers ::normalize
+ */
+ public function testNormalize() {
+ $expected = ['value' => '2016-11-06T09:02:00+00:00', 'format' => \DateTime::RFC3339];
+
+ $timestamp_item = $this->createTimestampItemProphecy();
+ $timestamp_item->getIterator()
+ ->willReturn(new \ArrayIterator(['value' => 1478422920]));
+
+ $serializer = new Serializer();
+ $this->normalizer->setSerializer($serializer);
+
+ $normalized = $this->normalizer->normalize($timestamp_item->reveal());
+ $this->assertSame($expected, $normalized);
+ }
+
+ /**
+ * Tests the denormalize function with good data.
+ *
+ * @covers ::denormalize
+ * @dataProvider providerTestDenormalizeValidFormats
+ */
+ public function testDenormalizeValidFormats($value, $expected) {
+ $normalized = ['value' => $value];
+
+ $timestamp_item = $this->createTimestampItemProphecy();
+ // The field item should be set with the expected timestamp.
+ $timestamp_item->setValue(['value' => $expected])
+ ->shouldBeCalled();
+
+ $context = ['target_instance' => $timestamp_item->reveal()];
+
+ $denormalized = $this->normalizer->denormalize($normalized, TimestampItem::class, NULL, $context);
+ $this->assertTrue($denormalized instanceof TimestampItem);
+ }
+
+ /**
+ * Data provider for testDenormalizeValidFormats.
+ *
+ * @return array
+ */
+ public function providerTestDenormalizeValidFormats() {
+ $expected_stamp = 1478422920;
+
+ $data = [];
+
+ $data['U'] = [$expected_stamp, $expected_stamp];
+ $data['RFC3339'] = ['2016-11-06T09:02:00+00:00', $expected_stamp];
+ $data['RFC3339 +0100'] = ['2016-11-06T09:02:00+01:00', $expected_stamp - 1 * 3600];
+ $data['RFC3339 -0600'] = ['2016-11-06T09:02:00-06:00', $expected_stamp + 6 * 3600];
+
+ $data['ISO8601'] = ['2016-11-06T09:02:00+0000', $expected_stamp];
+ $data['ISO8601 +0100'] = ['2016-11-06T09:02:00+0100', $expected_stamp - 1 * 3600];
+ $data['ISO8601 -0600'] = ['2016-11-06T09:02:00-0600', $expected_stamp + 6 * 3600];
+
+ return $data;
+ }
+
+ /**
+ * Tests the denormalize function with bad data.
+ *
+ * @covers ::denormalize
+ */
+ public function testDenormalizeException() {
+ $this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016/11/06 09:02am GMT" is not in an accepted format: "U" (UNIX timestamp), "Y-m-d\TH:i:sO" (ISO 8601), "Y-m-d\TH:i:sP" (RFC 3339).');
+
+ $context = ['target_instance' => $this->createTimestampItemProphecy()->reveal()];
+
+ $normalized = ['value' => '2016/11/06 09:02am GMT'];
+ $this->normalizer->denormalize($normalized, TimestampItem::class, NULL, $context);
+ }
+
+ /**
+ * Creates a TimestampItem prophecy.
+ *
+ * @return \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem
+ */
+ protected function createTimestampItemProphecy() {
+ $timestamp_item = $this->prophesize(TimestampItem::class);
+ $timestamp_item->getParent()
+ ->willReturn(TRUE);
+
+ return $timestamp_item;
+ }
+
+}