LinkCollectionNormalizer.php

Same filename in other branches
  1. 9 core/modules/jsonapi/src/Normalizer/LinkCollectionNormalizer.php
  2. 8.9.x core/modules/jsonapi/src/Normalizer/LinkCollectionNormalizer.php
  3. 11.x core/modules/jsonapi/src/Normalizer/LinkCollectionNormalizer.php

Namespace

Drupal\jsonapi\Normalizer

File

core/modules/jsonapi/src/Normalizer/LinkCollectionNormalizer.php

View source
<?php

namespace Drupal\jsonapi\Normalizer;

use Drupal\Component\Utility\Crypt;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Session\AccountInterface;
use Drupal\jsonapi\JsonApiResource\LinkCollection;
use Drupal\jsonapi\JsonApiResource\Link;
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
use Drupal\jsonapi\Normalizer\Value\CacheableOmission;

/**
 * Normalizes a LinkCollection object.
 *
 * The JSON:API specification has the concept of a "links collection". A links
 * collection is a JSON object where each member of the object is a
 * "link object". Unfortunately, this means that it is not possible to have more
 * than one link for a given key.
 *
 * 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. See this class's hashByHref() method for
 * details.
 *
 * This may change with a later version of the JSON:API specification.
 *
 * @internal JSON:API maintains no PHP API since its API is the HTTP API. This
 *   class may change at any time and this will break any dependencies on it.
 *
 * @see https://www.drupal.org/project/drupal/issues/3032787
 * @see jsonapi.api.php
 */
class LinkCollectionNormalizer extends NormalizerBase {
    
    /**
     * The normalizer $context key name for the key of an individual link.
     *
     * @var string
     */
    const LINK_KEY = 'jsonapi_links_object_link_key';
    
    /**
     * The normalizer $context key name for the context object of the link.
     *
     * @var string
     */
    const LINK_CONTEXT = 'jsonapi_links_object_context';
    
    /**
     * A random string to use when hashing links.
     *
     * This string is unique per instance of a link collection, but always the
     * same within it. This means that link key hashes will be non-deterministic
     * for outside observers, but two links within the same collection will always
     * have the same hash value.
     *
     * This is not used for cryptographic purposes.
     *
     * @var string
     */
    protected $hashSalt;
    
    /**
     * The current user making the request.
     *
     * @var \Drupal\Core\Session\AccountInterface
     */
    protected $currentUser;
    
    /**
     * LinkCollectionNormalizer constructor.
     *
     * @param \Drupal\Core\Session\AccountInterface $current_user
     *   The current user.
     */
    public function __construct(AccountInterface $current_user) {
        $this->currentUser = $current_user;
    }
    
    /**
     * {@inheritdoc}
     */
    public function normalize($object, $format = NULL, array $context = []) : array|string|int|float|bool|\ArrayObject|null {
        assert($object instanceof LinkCollection);
        $normalized = [];
        
        /** @var \Drupal\jsonapi\JsonApiResource\Link $link */
        foreach ($object as $key => $links) {
            $is_multiple = count($links) > 1;
            foreach ($links as $link) {
                $link_key = $is_multiple ? sprintf('%s--%s', $key, $this->hashByHref($link)) : $key;
                $attributes = $link->getTargetAttributes();
                $normalization = array_merge([
                    'href' => $link->getHref(),
                ], !empty($attributes) ? [
                    'meta' => $attributes,
                ] : []);
                // Checking access on links is not about access to the link itself;
                // it is about whether the current user has access to the route that is
                // *targeted* by the link. This is done on a "best effort" basis. That
                // is, some links target routes that depend on a request to determine if
                // they're accessible or not. Some other links might target routes to
                // which the current user will clearly not have access, in that case
                // this code proactively removes those links from the response.
                $access = $link->getUri()
                    ->access($this->currentUser, TRUE);
                $cacheability = CacheableMetadata::createFromObject($link)->addCacheableDependency($access);
                $normalized[$link_key] = $access->isAllowed() ? new CacheableNormalization($cacheability, $normalization) : new CacheableOmission($cacheability);
            }
        }
        return CacheableNormalization::aggregate($normalized);
    }
    
    /**
     * 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.
     *
     * @return string
     *   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();
        }
        // 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);
    }
    
    /**
     * {@inheritdoc}
     */
    public function hasCacheableSupportsMethod() : bool {
        @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use getSupportedTypes() instead. See https://www.drupal.org/node/3359695', E_USER_DEPRECATED);
        return TRUE;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getSupportedTypes(?string $format) : array {
        return [
            LinkCollection::class => TRUE,
        ];
    }

}

Classes

Title Deprecated Summary
LinkCollectionNormalizer Normalizes a LinkCollection object.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.