Unverified Commit 849ca9b5 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3175884 by gabesullice, mglaman, juagarc4, catch: JSON:API link keys can collide

(cherry picked from commit 99b763a1)
parent 9719c35a
Loading
Loading
Loading
Loading
+25 −3
Original line number Diff line number Diff line
@@ -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);
  }

}
+76 −0
Original line number Diff line number Diff line
<?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));
  }

}