class ActiveLinkResponseFilter

Same name and namespace in other branches
  1. 9 core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter
  2. 8.9.x core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter
  3. 10 core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter

Subscribes to filter HTML responses, to set attributes on active links.

Sets the 'is-active' class and sets the aria-current attribute to 'page'.

Only for anonymous users; for authenticated users, the active-link asset library is loaded.

Hierarchy

  • class \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter implements \Symfony\Component\EventDispatcher\EventSubscriberInterface

Expanded class hierarchy of ActiveLinkResponseFilter

See also

system_page_attachments()

1 file declares its use of ActiveLinkResponseFilter
ActiveLinkResponseFilterTest.php in core/tests/Drupal/Tests/Core/EventSubscriber/ActiveLinkResponseFilterTest.php

File

core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php, line 26

Namespace

Drupal\Core\EventSubscriber
View source
class ActiveLinkResponseFilter implements EventSubscriberInterface {
    
    /**
     * The current user.
     *
     * @var \Drupal\Core\Session\AccountInterface
     */
    protected $currentUser;
    
    /**
     * The current path.
     *
     * @var \Drupal\Core\Path\CurrentPathStack
     */
    protected $currentPath;
    
    /**
     * The path matcher.
     *
     * @var \Drupal\Core\Path\PathMatcherInterface
     */
    protected $pathMatcher;
    
    /**
     * The language manager.
     *
     * @var \Drupal\Core\Language\LanguageManagerInterface
     */
    protected $languageManager;
    
    /**
     * Constructs a new ActiveLinkResponseFilter instance.
     *
     * @param \Drupal\Core\Session\AccountInterface $current_user
     *   The current user.
     * @param \Drupal\Core\Path\CurrentPathStack $current_path
     *   The current path.
     * @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
     *   The path matcher.
     * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
     *   The language manager.
     */
    public function __construct(AccountInterface $current_user, CurrentPathStack $current_path, PathMatcherInterface $path_matcher, LanguageManagerInterface $language_manager) {
        $this->currentUser = $current_user;
        $this->currentPath = $current_path;
        $this->pathMatcher = $path_matcher;
        $this->languageManager = $language_manager;
    }
    
    /**
     * Sets the 'is-active' class on links.
     *
     * @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
     *   The response event.
     */
    public function onResponse(ResponseEvent $event) {
        $response = $event->getResponse();
        // Only care about HTML responses.
        if (stripos($response->headers
            ->get('Content-Type', ''), 'text/html') === FALSE) {
            return;
        }
        // For authenticated users, the 'is-active' class is set in JavaScript.
        // @see system_page_attachments()
        if ($this->currentUser
            ->isAuthenticated()) {
            return;
        }
        // If content is FALSE, assume the response does not support the
        // setContent() method and skip it, for example,
        // \Symfony\Component\HttpFoundation\BinaryFileResponse.
        $content = $response->getContent();
        if ($content !== FALSE) {
            $response->setContent(static::setLinkActiveClass($content, ltrim($this->currentPath
                ->getPath(), '/'), $this->pathMatcher
                ->isFrontPage(), $this->languageManager
                ->getCurrentLanguage(LanguageInterface::TYPE_URL)
                ->getId(), $event->getRequest()->query
                ->all()));
        }
    }
    
    /**
     * Sets the "is-active" class and aria-current attribute on relevant links.
     *
     * This is a PHP implementation of the drupal.active-link JavaScript library.
     *
     * @param string $html_markup
     *   The HTML markup to update.
     * @param string $current_path
     *   The system path of the currently active page.
     * @param bool $is_front
     *   Whether the current page is the front page (which implies the current
     *   path might also be <front>).
     * @param string $url_language
     *   The language code of the current URL.
     * @param array $query
     *   The query string for the current URL.
     *
     * @return string
     *   The updated HTML markup.
     *
     * @todo Once a future version of PHP supports parsing HTML5 properly
     *   (i.e. doesn't fail on
     *   https://www.drupal.org/comment/7938201#comment-7938201) then we can get
     *   rid of this manual parsing and use DOMDocument instead.
     */
    public static function setLinkActiveClass($html_markup, $current_path, $is_front, $url_language, array $query) {
        $search_key_current_path = 'data-drupal-link-system-path="' . $current_path . '"';
        $search_key_front = 'data-drupal-link-system-path="&lt;front&gt;"';
        // Receive the query in a standardized manner.
        ksort($query);
        $offset = 0;
        // There are two distinct conditions that can make a link be marked active:
        // 1. A link has the current path in its 'data-drupal-link-system-path'
        //    attribute.
        // 2. We are on the front page and a link has the special '<front>' value in
        //    its 'data-drupal-link-system-path' attribute.
        while (str_contains(substr($html_markup, $offset), $search_key_current_path) || $is_front && str_contains(substr($html_markup, $offset), $search_key_front)) {
            $pos_current_path = strpos($html_markup, $search_key_current_path, $offset);
            // Only look for links with the special '<front>' system path if we are
            // actually on the front page.
            $pos_front = $is_front ? strpos($html_markup, $search_key_front, $offset) : FALSE;
            // Determine which of the two values is the next match: the exact path, or
            // the <front> special case.
            $pos_match = NULL;
            if ($pos_front === FALSE) {
                $pos_match = $pos_current_path;
            }
            elseif ($pos_current_path === FALSE) {
                $pos_match = $pos_front;
            }
            elseif ($pos_current_path < $pos_front) {
                $pos_match = $pos_current_path;
            }
            else {
                $pos_match = $pos_front;
            }
            // Find beginning and ending of opening tag.
            $pos_tag_start = NULL;
            for ($i = $pos_match; $pos_tag_start === NULL && $i > 0; $i--) {
                if ($html_markup[$i] === '<') {
                    $pos_tag_start = $i;
                }
            }
            $pos_tag_end = NULL;
            for ($i = $pos_match; $pos_tag_end === NULL && $i < strlen($html_markup); $i++) {
                if ($html_markup[$i] === '>') {
                    $pos_tag_end = $i;
                }
            }
            // Get the HTML: this will be the opening part of a single tag, e.g.:
            // <a href="/" data-drupal-link-system-path="&lt;front&gt;">
            $tag = substr($html_markup, $pos_tag_start ?? 0, $pos_tag_end - $pos_tag_start + 1);
            // Parse it into a DOMDocument so we can reliably read and modify
            // attributes.
            $dom = new \DOMDocument();
            @$dom->loadHTML('<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>' . $tag . '</body></html>');
            $node = $dom->getElementsByTagName('body')
                ->item(0)->firstChild;
            // Ensure we don't set the "active" class twice on the same element.
            $class = $node->getAttribute('class');
            $add_active = !in_array('is-active', explode(' ', $class));
            // The language of an active link is equal to the current language.
            if ($add_active && $url_language) {
                if ($node->hasAttribute('hreflang') && $node->getAttribute('hreflang') !== $url_language) {
                    $add_active = FALSE;
                }
            }
            // The query parameters of an active link are equal to the current
            // parameters.
            if ($add_active) {
                if ($query) {
                    if (!$node->hasAttribute('data-drupal-link-query') || $node->getAttribute('data-drupal-link-query') !== Json::encode($query)) {
                        $add_active = FALSE;
                    }
                }
                else {
                    if ($node->hasAttribute('data-drupal-link-query')) {
                        $add_active = FALSE;
                    }
                }
            }
            // Only if the path, the language and the query match, we set the
            // "is-active" class and add aria-current="page".
            if ($add_active) {
                if (strlen($class) > 0) {
                    $class .= ' ';
                }
                $class .= 'is-active';
                $node->setAttribute('class', $class);
                $node->setAttribute('aria-current', 'page');
                // Get the updated tag.
                $updated_tag = $dom->saveXML($node, LIBXML_NOEMPTYTAG);
                // saveXML() added a closing tag, remove it.
                $updated_tag = substr($updated_tag, 0, strrpos($updated_tag, '<'));
                $html_markup = str_replace($tag, $updated_tag, $html_markup);
                // Ensure we only search the remaining HTML.
                $offset = $pos_tag_end - strlen($tag) + strlen($updated_tag);
            }
            else {
                // Ensure we only search the remaining HTML.
                $offset = $pos_tag_end + 1;
            }
        }
        return $html_markup;
    }
    
    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents() : array {
        // Should run after any other response subscriber that modifies the markup.
        $events[KernelEvents::RESPONSE][] = [
            'onResponse',
            -512,
        ];
        return $events;
    }

}

Members

Title Sort descending Modifiers Object type Summary
ActiveLinkResponseFilter::$currentPath protected property The current path.
ActiveLinkResponseFilter::$currentUser protected property The current user.
ActiveLinkResponseFilter::$languageManager protected property The language manager.
ActiveLinkResponseFilter::$pathMatcher protected property The path matcher.
ActiveLinkResponseFilter::getSubscribedEvents public static function
ActiveLinkResponseFilter::onResponse public function Sets the &#039;is-active&#039; class on links.
ActiveLinkResponseFilter::setLinkActiveClass public static function Sets the &quot;is-active&quot; class and aria-current attribute on relevant links.
ActiveLinkResponseFilter::__construct public function Constructs a new ActiveLinkResponseFilter instance.

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