b32cf84e22a4736ff2840a8b2a8a44d6d05bd5c6
[yaffs-website] / web / core / lib / Drupal / Core / EventSubscriber / ActiveLinkResponseFilter.php
1 <?php
2
3 namespace Drupal\Core\EventSubscriber;
4
5 use Drupal\Component\Serialization\Json;
6 use Drupal\Core\Language\LanguageInterface;
7 use Drupal\Core\Language\LanguageManagerInterface;
8 use Drupal\Core\Path\CurrentPathStack;
9 use Drupal\Core\Path\PathMatcherInterface;
10 use Drupal\Core\Session\AccountInterface;
11 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
12 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
13 use Symfony\Component\HttpKernel\KernelEvents;
14
15 /**
16  * Subscribes to filter HTML responses, to set the 'is-active' class on links.
17  *
18  * Only for anonymous users; for authenticated users, the active-link asset
19  * library is loaded.
20  *
21  * @see system_page_attachments()
22  */
23 class ActiveLinkResponseFilter implements EventSubscriberInterface {
24
25   /**
26    * The current user.
27    *
28    * @var \Drupal\Core\Session\AccountInterface
29    */
30   protected $currentUser;
31
32   /**
33    * The current path.
34    *
35    * @var \Drupal\Core\Path\CurrentPathStack
36    */
37   protected $currentPath;
38
39   /**
40    * The path matcher.
41    *
42    * @var \Drupal\Core\Path\PathMatcherInterface
43    */
44   protected $pathMatcher;
45
46   /**
47    * The language manager.
48    *
49    * @var \Drupal\Core\Language\LanguageManagerInterface
50    */
51   protected $languageManager;
52
53   /**
54    * Constructs a new ActiveLinkResponseFilter instance.
55    *
56    * @param \Drupal\Core\Session\AccountInterface $current_user
57    *   The current user.
58    * @param \Drupal\Core\Path\CurrentPathStack $current_path
59    *   The current path.
60    * @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
61    *   The path matcher.
62    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
63    *   The language manager.
64    */
65   public function __construct(AccountInterface $current_user, CurrentPathStack $current_path, PathMatcherInterface $path_matcher, LanguageManagerInterface $language_manager) {
66     $this->currentUser = $current_user;
67     $this->currentPath = $current_path;
68     $this->pathMatcher = $path_matcher;
69     $this->languageManager = $language_manager;
70   }
71
72   /**
73    * Sets the 'is-active' class on links.
74    *
75    * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
76    *   The response event.
77    */
78   public function onResponse(FilterResponseEvent $event) {
79     // Only care about HTML responses.
80     if (stripos($event->getResponse()->headers->get('Content-Type'), 'text/html') === FALSE) {
81       return;
82     }
83
84     // For authenticated users, the 'is-active' class is set in JavaScript.
85     // @see system_page_attachments()
86     if ($this->currentUser->isAuthenticated()) {
87       return;
88     }
89
90     $response = $event->getResponse();
91     $response->setContent(static::setLinkActiveClass(
92       $response->getContent(),
93       ltrim($this->currentPath->getPath(), '/'),
94       $this->pathMatcher->isFrontPage(),
95       $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId(),
96       $event->getRequest()->query->all()
97     ));
98   }
99
100   /**
101    * Sets the "is-active" class on relevant links.
102    *
103    * This is a PHP implementation of the drupal.active-link JavaScript library.
104    *
105    * @param string $html_markup
106    *   The HTML markup to update.
107    * @param string $current_path
108    *   The system path of the currently active page.
109    * @param bool $is_front
110    *   Whether the current page is the front page (which implies the current
111    *   path might also be <front>).
112    * @param string $url_language
113    *   The language code of the current URL.
114    * @param array $query
115    *   The query string for the current URL.
116    *
117    * @return string
118    *   The updated HTML markup.
119    *
120    * @todo Once a future version of PHP supports parsing HTML5 properly
121    *   (i.e. doesn't fail on
122    *   https://www.drupal.org/comment/7938201#comment-7938201) then we can get
123    *   rid of this manual parsing and use DOMDocument instead.
124    */
125   public static function setLinkActiveClass($html_markup, $current_path, $is_front, $url_language, array $query) {
126     $search_key_current_path = 'data-drupal-link-system-path="' . $current_path . '"';
127     $search_key_front = 'data-drupal-link-system-path="&lt;front&gt;"';
128
129     // Receive the query in a standardized manner.
130     ksort($query);
131
132     $offset = 0;
133     // There are two distinct conditions that can make a link be marked active:
134     // 1. A link has the current path in its 'data-drupal-link-system-path'
135     //    attribute.
136     // 2. We are on the front page and a link has the special '<front>' value in
137     //    its 'data-drupal-link-system-path' attribute.
138     while (strpos($html_markup, $search_key_current_path, $offset) !== FALSE || ($is_front && strpos($html_markup, $search_key_front, $offset) !== FALSE)) {
139       $pos_current_path = strpos($html_markup, $search_key_current_path, $offset);
140       // Only look for links with the special '<front>' system path if we are
141       // actually on the front page.
142       $pos_front = $is_front ? strpos($html_markup, $search_key_front, $offset) : FALSE;
143
144       // Determine which of the two values is the next match: the exact path, or
145       // the <front> special case.
146       $pos_match = NULL;
147       if ($pos_front === FALSE) {
148         $pos_match = $pos_current_path;
149       }
150       elseif ($pos_current_path === FALSE) {
151         $pos_match = $pos_front;
152       }
153       elseif ($pos_current_path < $pos_front) {
154         $pos_match = $pos_current_path;
155       }
156       else {
157         $pos_match = $pos_front;
158       }
159
160       // Find beginning and ending of opening tag.
161       $pos_tag_start = NULL;
162       for ($i = $pos_match; $pos_tag_start === NULL && $i > 0; $i--) {
163         if ($html_markup[$i] === '<') {
164           $pos_tag_start = $i;
165         }
166       }
167       $pos_tag_end = NULL;
168       for ($i = $pos_match; $pos_tag_end === NULL && $i < strlen($html_markup); $i++) {
169         if ($html_markup[$i] === '>') {
170           $pos_tag_end = $i;
171         }
172       }
173
174       // Get the HTML: this will be the opening part of a single tag, e.g.:
175       //   <a href="/" data-drupal-link-system-path="&lt;front&gt;">
176       $tag = substr($html_markup, $pos_tag_start, $pos_tag_end - $pos_tag_start + 1);
177
178       // Parse it into a DOMDocument so we can reliably read and modify
179       // attributes.
180       $dom = new \DOMDocument();
181       @$dom->loadHTML('<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>' . $tag . '</body></html>');
182       $node = $dom->getElementsByTagName('body')->item(0)->firstChild;
183
184       // Ensure we don't set the "active" class twice on the same element.
185       $class = $node->getAttribute('class');
186       $add_active = !in_array('is-active', explode(' ', $class));
187
188       // The language of an active link is equal to the current language.
189       if ($add_active && $url_language) {
190         if ($node->hasAttribute('hreflang') && $node->getAttribute('hreflang') !== $url_language) {
191           $add_active = FALSE;
192         }
193       }
194       // The query parameters of an active link are equal to the current
195       // parameters.
196       if ($add_active) {
197         if ($query) {
198           if (!$node->hasAttribute('data-drupal-link-query') || $node->getAttribute('data-drupal-link-query') !== Json::encode($query)) {
199             $add_active = FALSE;
200           }
201         }
202         else {
203           if ($node->hasAttribute('data-drupal-link-query')) {
204             $add_active = FALSE;
205           }
206         }
207       }
208
209       // Only if the path, the language and the query match, we set the
210       // "is-active" class.
211       if ($add_active) {
212         if (strlen($class) > 0) {
213           $class .= ' ';
214         }
215         $class .= 'is-active';
216         $node->setAttribute('class', $class);
217
218         // Get the updated tag.
219         $updated_tag = $dom->saveXML($node, LIBXML_NOEMPTYTAG);
220         // saveXML() added a closing tag, remove it.
221         $updated_tag = substr($updated_tag, 0, strrpos($updated_tag, '<'));
222
223         $html_markup = str_replace($tag, $updated_tag, $html_markup);
224
225         // Ensure we only search the remaining HTML.
226         $offset = $pos_tag_end - strlen($tag) + strlen($updated_tag);
227       }
228       else {
229         // Ensure we only search the remaining HTML.
230         $offset = $pos_tag_end + 1;
231       }
232     }
233
234     return $html_markup;
235   }
236
237   /**
238    * {@inheritdoc}
239    */
240   public static function getSubscribedEvents() {
241     // Should run after any other response subscriber that modifies the markup.
242     $events[KernelEvents::RESPONSE][] = ['onResponse', -512];
243
244     return $events;
245   }
246
247 }