diff --git a/core/modules/jsonapi/src/Normalizer/LinkCollectionNormalizer.php b/core/modules/jsonapi/src/Normalizer/LinkCollectionNormalizer.php index 2c3f5b71de4665fc548e21e40939704d3cc334ad..a2440a9aff2a8a432af6032c76ec2d03896325da 100644 --- a/core/modules/jsonapi/src/Normalizer/LinkCollectionNormalizer.php +++ b/core/modules/jsonapi/src/Normalizer/LinkCollectionNormalizer.php @@ -17,7 +17,8 @@ * * When normalizing more than one link in a LinkCollection with the same key, a * unique and random string is appended to the link's key after a double dash - * (--) to differentiate the links. + * (--) to differentiate the links. See this class's hashByHref() method for + * details. * * This may change with a later version of the JSON:API specification. * @@ -82,7 +83,15 @@ public function normalize($object, $format = NULL, array $context = []) { } /** - * Hashes a link by its href. + * Hashes a link using its href and its target attributes, if any. + * + * This method generates an unpredictable, but deterministic, 7 character + * alphanumeric hash for a given link. + * + * The hash is unpredictable because a random hash salt will be used for every + * request. The hash is deterministic because, within a single request, links + * with the same href and target attributes (i.o.w. duplicates) will generate + * equivalent hash values. * * @param \Drupal\jsonapi\JsonApiResource\Link $link * A link to be hashed. @@ -91,10 +100,23 @@ public function normalize($object, $format = NULL, array $context = []) { * A 7 character alphanumeric hash. */ protected function hashByHref(Link $link) { + // Generate a salt unique to each instance of this class. if (!$this->hashSalt) { $this->hashSalt = Crypt::randomBytesBase64(); } - return substr(str_replace(['-', '_'], '', Crypt::hashBase64($this->hashSalt . $link->getHref())), 0, 7); + // Create a dictionary of link parameters. + $link_parameters = [ + 'href' => $link->getHref(), + ] + $link->getTargetAttributes(); + // Serialize the dictionary into a string. + foreach ($link_parameters as $name => $value) { + $serialized_parameters[] = sprintf('%s="%s"', $name, implode(' ', (array) $value)); + } + // Hash the string. + $b64_hash = Crypt::hashBase64($this->hashSalt . implode('; ', $serialized_parameters)); + // Remove any dashes and underscores from the base64 hash and then return + // the first 7 characters. + return substr(str_replace(['-', '_'], '', $b64_hash), 0, 7); } } diff --git a/core/modules/jsonapi/tests/src/Kernel/Normalizer/LinkCollectionNormalizerTest.php b/core/modules/jsonapi/tests/src/Kernel/Normalizer/LinkCollectionNormalizerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9385b09749f309804c7e67c60d37980715bf5060 --- /dev/null +++ b/core/modules/jsonapi/tests/src/Kernel/Normalizer/LinkCollectionNormalizerTest.php @@ -0,0 +1,76 @@ +<?php + +namespace Drupal\Tests\jsonapi\Kernel\Normalizer; + +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Url; +use Drupal\jsonapi\JsonApiResource\Link; +use Drupal\jsonapi\JsonApiResource\LinkCollection; +use Drupal\jsonapi\JsonApiResource\ResourceObject; +use Drupal\jsonapi\Normalizer\LinkCollectionNormalizer; +use Drupal\jsonapi\ResourceType\ResourceType; +use Drupal\KernelTests\KernelTestBase; + +/** + * @coversDefaultClass \Drupal\jsonapi\Normalizer\LinkCollectionNormalizer + * @group jsonapi + * + * @internal + */ +class LinkCollectionNormalizerTest extends KernelTestBase { + + /** + * The subject under test. + * + * @var \Symfony\Component\Serializer\Normalizer\NormalizerInterface + */ + protected $normalizer; + + /** + * {@inheritDoc} + */ + protected static $modules = [ + 'jsonapi', + 'serialization', + ]; + + /** + * {@inheritDoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->normalizer = new LinkCollectionNormalizer(); + $this->normalizer->setSerializer($this->container->get('jsonapi.serializer')); + } + + /** + * Tests the link collection normalizer. + */ + public function testNormalize() { + $link_context = new ResourceObject(new CacheableMetadata(), new ResourceType('n/a', 'n/a', 'n/a'), 'n/a', NULL, [], new LinkCollection([])); + $link_collection = (new LinkCollection([])) + ->withLink('related', new Link(new CacheableMetadata(), Url::fromUri('http://example.com/post/42'), 'related', ['title' => 'Most viewed'])) + ->withLink('related', new Link(new CacheableMetadata(), Url::fromUri('http://example.com/post/42'), 'related', ['title' => 'Top rated'])) + ->withContext($link_context); + $normalized = $this->normalizer->normalize($link_collection)->getNormalization(); + $this->assertIsArray($normalized); + foreach (array_keys($normalized) as $key) { + $this->assertStringStartsWith('related', $key); + } + $this->assertSame([ + [ + 'href' => 'http://example.com/post/42', + 'meta' => [ + 'title' => 'Most viewed', + ], + ], + [ + 'href' => 'http://example.com/post/42', + 'meta' => [ + 'title' => 'Top rated', + ], + ], + ], array_values($normalized)); + } + +}