From 99b763a12ac9cbe49ab099589f5bbb245825567a Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Tue, 17 Nov 2020 11:20:53 +0000 Subject: [PATCH] Issue #3175884 by gabesullice, mglaman, juagarc4, catch: JSON:API link keys can collide --- .../Normalizer/LinkCollectionNormalizer.php | 28 ++++++- .../LinkCollectionNormalizerTest.php | 76 +++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 core/modules/jsonapi/tests/src/Kernel/Normalizer/LinkCollectionNormalizerTest.php diff --git a/core/modules/jsonapi/src/Normalizer/LinkCollectionNormalizer.php b/core/modules/jsonapi/src/Normalizer/LinkCollectionNormalizer.php index 2c3f5b71de4..a2440a9aff2 100644 --- a/core/modules/jsonapi/src/Normalizer/LinkCollectionNormalizer.php +++ b/core/modules/jsonapi/src/Normalizer/LinkCollectionNormalizer.php @@ -17,7 +17,8 @@ use Drupal\jsonapi\Normalizer\Value\CacheableNormalization; * * 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 @@ class LinkCollectionNormalizer extends NormalizerBase { } /** - * 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 @@ class LinkCollectionNormalizer extends NormalizerBase { * 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 00000000000..9385b09749f --- /dev/null +++ b/core/modules/jsonapi/tests/src/Kernel/Normalizer/LinkCollectionNormalizerTest.php @@ -0,0 +1,76 @@ +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)); + } + +}