Skip to content
Snippets Groups Projects
Commit 3a605ea9 authored by Patrick Fey's avatar Patrick Fey Committed by Patrick Fey
Browse files

Issue #3250223 by FeyP, Yogesh Pawar, bigboy: Incorrect path alias and "token" parameter in links

parent 4fbe09b7
No related branches found
No related tags found
No related merge requests found
......@@ -9,10 +9,13 @@ use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*
* @phpstan-param string $route_name
* @phpstan-return array<mixed>
*/
function language_switcher_menu_help($route_name, RouteMatchInterface $route_match) {
if ($route_name !== 'help.page.language_switcher_menu') {
return;
return [];
}
$build = [];
......
......@@ -5,3 +5,4 @@ services:
- '@access_manager'
- '@current_user'
- '@entity_type.manager'
- '@path.validator'
......@@ -68,6 +68,8 @@ class SettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*
* @phpstan-return self
*/
public static function create(ContainerInterface $container) {
return new static(
......@@ -81,6 +83,8 @@ class SettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*
* @phpstan-return array<string>
*/
protected function getEditableConfigNames() {
return [
......@@ -90,6 +94,8 @@ class SettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*
* @phpstan-return string
*/
public function getFormId() {
return 'language_switcher_menu_settings_form';
......@@ -97,6 +103,9 @@ class SettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*
* @phpstan-param array<mixed> $form
* @phpstan-return array<mixed>
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('language_switcher_menu.settings');
......@@ -109,7 +118,7 @@ class SettingsForm extends ConfigFormBase {
}
$default = $config->get('type') ? $config->get('type') : NULL;
if (empty($default) && count($configurable_types) === 1) {
$default = isset($type) ? $type : NULL;
$default = $type ?? NULL;
}
$form['type'] = [
'#type' => 'select',
......@@ -166,6 +175,9 @@ class SettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*
* @phpstan-param array<mixed> $form
* @phpstan-return void
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
......
......@@ -2,38 +2,65 @@
namespace Drupal\language_switcher_menu;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Menu\DefaultMenuLinkTreeManipulators;
use Drupal\Core\Menu\MenuLinkInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\language_switcher_menu\Plugin\Menu\LanguageSwitcherLink;
/**
* Extends access check tree manipulator provided by Drupal Core.
*
* @todo Remove once https://www.drupal.org/project/drupal/issues/3008889 has
* @todo Revisit once https://www.drupal.org/project/drupal/issues/3008889 has
* been fixed.
*/
class LanguageLinkAccessMenuTreeManipulator extends DefaultMenuLinkTreeManipulators {
/**
* The path validator.
*
* @var \Drupal\Core\Path\PathValidatorInterface
*/
protected $pathValidator;
/**
* Constructs a LanguageLinkAccessMenuTreeManipulator object.
*
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager
* The access manager.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Path\PathValidatorInterface $path_validator
* The path validator.
*/
public function __construct(AccessManagerInterface $access_manager, AccountInterface $account, EntityTypeManagerInterface $entity_type_manager, PathValidatorInterface $path_validator) {
parent::__construct($access_manager, $account, $entity_type_manager);
$this->pathValidator = $path_validator;
}
/**
* {@inheritdoc}
*/
protected function menuLinkCheckAccess(MenuLinkInterface $instance) {
$access_result = parent::menuLinkCheckAccess($instance);
if (!$access_result->isNeutral()) {
if (!$instance instanceof LanguageSwitcherLink) {
return $access_result;
}
if ($instance->getUrlObject()->getRouteName() !== '<current>') {
return $access_result;
}
// @note Third party modules may alter language switch links and they would
// still be an instance of our class, so we check for it relatively late.
if (!$instance instanceof LanguageSwitcherLink) {
return $access_result;
if (!$instance->hasLink()) {
return $access_result->isAllowed() ? AccessResult::neutral() : $access_result;
}
$has_access = $this->account->hasPermission('view language_switcher_menu links');
return AccessResult::allowedIf($has_access)->cachePerPermissions();
$url = $instance->getUrlObject()->setAbsolute(TRUE)->toString();
$validated_url = $this->pathValidator->getUrlIfValid($url);
return AccessResult::allowedIfHasPermission($this->account, 'view language_switcher_menu links')
->andIf(AccessResult::allowedIf($validated_url));
}
}
......@@ -5,9 +5,7 @@ namespace Drupal\language_switcher_menu\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Path\PathMatcherInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -29,13 +27,6 @@ class LanguageSwitcherLink extends DeriverBase implements ContainerDeriverInterf
*/
protected $languageManager;
/**
* The path matcher.
*
* @var \Drupal\Core\Path\PathMatcherInterface
*/
protected $pathMatcher;
/**
* Constructs a new LanguageSwitcherLink instance.
*
......@@ -43,13 +34,10 @@ class LanguageSwitcherLink extends DeriverBase implements ContainerDeriverInterf
* The factory for configuration objects.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
* The path matcher.
*/
public function __construct(ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager, PathMatcherInterface $path_matcher) {
public function __construct(ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager) {
$this->configFactory = $config_factory;
$this->languageManager = $language_manager;
$this->pathMatcher = $path_matcher;
}
/**
......@@ -58,13 +46,15 @@ class LanguageSwitcherLink extends DeriverBase implements ContainerDeriverInterf
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('config.factory'),
$container->get('language_manager'),
$container->get('path.matcher'),
$container->get('language_manager')
);
}
/**
* {@inheritdoc}
*
* @phpstan-param mixed $base_plugin_definition
* @phpstan-return array<string, mixed>
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$links = [];
......@@ -82,12 +72,6 @@ class LanguageSwitcherLink extends DeriverBase implements ContainerDeriverInterf
return $links;
}
$route_name = $this->pathMatcher->isFrontPage() ? '<front>' : '<current>';
$link_info = $this->languageManager->getLanguageSwitchLinks($config->get('type'), Url::fromRoute($route_name));
if (!isset($link_info->links)) {
return $links;
}
$parent_config = explode(':', $config->get('parent'));
$menu_name = $parent_config[0];
$parent = NULL;
......@@ -97,23 +81,22 @@ class LanguageSwitcherLink extends DeriverBase implements ContainerDeriverInterf
}
$weight = (int) $config->get('weight');
foreach ($link_info->links as $id => $link) {
$links[$id] = [
'title' => $link['title'],
'route_name' => $link['url']->getRouteName(),
'route_parameters' => $link['url']->getRouteParameters(),
$type = $config->get('type');
foreach ($this->languageManager->getLanguages() as $langcode => $language) {
$links[$langcode] = [
'title' => $language->getName(),
'metadata' => [
'language' => $language,
'langcode' => $langcode,
'langtype' => $type,
],
'route_name' => '<current>',
'route_parameters' => [],
'menu_name' => $menu_name,
'parent' => $parent,
'weight' => $weight,
'options' => [
'language' => $link['language'],
'set_active_class' => TRUE,
] + (isset($link['query']) ? [
'query' => $link['query'],
] : []) + (isset($link['attributes']) ? [
'attributes' => $link['attributes'],
] : []),
'options' => [],
] + $base_plugin_definition;
$weight + 1;
}
......
......@@ -2,13 +2,188 @@
namespace Drupal\language_switcher_menu\Plugin\Menu;
use Drupal\Core\Url;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Menu\StaticMenuLinkOverridesInterface;
use Drupal\Core\Path\PathMatcherInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Represents a menu link to switch to a specific language.
*/
class LanguageSwitcherLink extends MenuLinkDefault {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The path matcher.
*
* @var \Drupal\Core\Path\PathMatcherInterface
*/
protected $pathMatcher;
/**
* Language switch links for active route.
*
* NULL, if never initialized. FALSE, if unsuccessfully initialized. An array
* of language switch links, if successfully initialized.
*
* @var array|null|false
* @phpstan-var array<string,mixed>|null|false
*/
protected $links = NULL;
/**
* Constructs a new LanguageSwitcherLink.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $static_override
* The static override storage.
* @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
* The path matcher.
*
* @phpstan-param array<mixed> $configuration
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, LanguageManagerInterface $language_manager, StaticMenuLinkOverridesInterface $static_override, PathMatcherInterface $path_matcher) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $static_override);
$this->languageManager = $language_manager;
$this->pathMatcher = $path_matcher;
}
/**
* {@inheritdoc}
*
* @phpstan-param array<mixed> $configuration
* @phpstan-param string $plugin_id
* @phpstan-param mixed $plugin_definition
* @phpstan-return self
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('language_manager'),
$container->get('menu_link.static.overrides'),
$container->get('path.matcher')
);
}
/**
* Initializes the "links" property.
*
* If the "links" property has not been initialized yet, gets links from
* language switcher and assigns them to the "links" property.
*/
protected function initLinks(): void {
if ($this->links !== NULL) {
return;
}
$route_name = $this->pathMatcher->isFrontPage() ? '<front>' : '<current>';
$link_info = $this->languageManager->getLanguageSwitchLinks($this->getLanguageType(), Url::fromRoute($route_name));
$this->links = $link_info->links ?? FALSE;
}
/**
* Whether a language switch link for this menu link's language code exists.
*
* @return bool
* Whether a language switch link for this menu link's language code exists.
*/
public function hasLink(): bool {
$this->initLinks();
return isset($this->links[$this->getLangCode()]);
}
/**
* Gets link for language code of this language switcher menu link.
*
* @return array
* Link for language code of this language switcher menu link.
*
* @phpstan-return array<mixed>
*/
protected function getLink() {
$this->initLinks();
return $this->hasLink() ? $this->links[$this->getLangCode()] : [];
}
/**
* Gets the language code.
*
* @return string
* Language code.
*/
protected function getLangCode(): string {
return $this->pluginDefinition['metadata']['langcode'];
}
/**
* Gets the language type.
*
* @return string
* Language type.
*/
protected function getLanguageType(): string {
return $this->pluginDefinition['metadata']['langtype'];
}
/**
* {@inheritdoc}
*/
public function getTitle() {
$link = $this->getLink();
return (string) ($link['title'] ?? parent::getTitle());
}
/**
* {@inheritdoc}
*
* @phpstan-return array<string, mixed>
*/
public function getOptions() {
$link = $this->getLink();
return [
'language' => $link['language'] ?? NULL,
'set_active_class' => TRUE,
] + (isset($link['query']) ? [
'query' => $link['query'],
] : []) + (isset($link['attributes']) ? [
'attributes' => $link['attributes'],
] : []) + parent::getOptions();
}
/**
* {@inheritdoc}
*/
public function getRouteName() {
$link = $this->getLink();
return isset($link['url']) ? $link['url']->getRouteName() : '';
}
/**
* {@inheritdoc}
*
* @phpstan-return array<string, mixed>
*/
public function getRouteParameters() {
$link = $this->getLink();
return isset($link['url']) ? $link['url']->getRouteParameters() : [];
}
/**
* {@inheritdoc}
*/
......
......@@ -70,7 +70,7 @@ class LanguageSwitcherMenuTest extends BrowserTestBase {
/**
* Tests language switch links provided by Language Switcher Menu module.
*/
public function testLanguageSwitchLinks() {
public function testLanguageSwitchLinks(): void {
$this->drupalLogin($this->users['admin_user']);
// Add a language.
......@@ -155,6 +155,8 @@ class LanguageSwitcherMenuTest extends BrowserTestBase {
$this->assertSession()->statusCodeEquals(403);
$this->drupalLogin($this->users['view_links']);
$this->drupalGet('/user/' . $this->users['view_links']->id());
$this->assertSession()->statusCodeEquals(200);
$this->assertMenuLinks([
'/user/' . $this->users['view_links']->id() => 'English',
'/fr/user/' . $this->users['view_links']->id() => 'French',
......@@ -168,16 +170,16 @@ class LanguageSwitcherMenuTest extends BrowserTestBase {
$this->drupalGet('user/' . $admin_id . '/edit');
$this->assertSession()->statusCodeEquals(403);
$this->assertMenuLinks([
'/system/403' => 'English',
'/fr/system/403' => 'French',
'/system/403?destination=/user/' . $admin_id . '/edit&_exception_statuscode=403' => 'English',
'/fr/system/403?destination=/user/' . $admin_id . '/edit&_exception_statuscode=403' => 'French',
'/' => 'Home',
], 'The language links are visible on pages that deny access.');
// Check that links are shown in a non-standard language.
$this->clickLink('French');
$this->assertMenuLinks([
'/system/403' => 'English',
'/fr/system/403' => 'French',
'/system/403?destination=/user/' . $admin_id . '/edit&_exception_statuscode=403' => 'English',
'/fr/system/403?destination=/user/' . $admin_id . '/edit&_exception_statuscode=403' => 'French',
'/fr' => 'Home',
], 'The menu links are correct in a non-standard language.');
......@@ -254,7 +256,7 @@ class LanguageSwitcherMenuTest extends BrowserTestBase {
* @param string $text
* Assert message.
*/
protected function assertMenuLinks(array $expected, string $text) {
protected function assertMenuLinks(array $expected, string $text): void {
$language_switchers = $this->xpath('//nav/ul/li');
$labels = [];
$urls = [];
......@@ -267,7 +269,13 @@ class LanguageSwitcherMenuTest extends BrowserTestBase {
// through \Drupal\Core\Url to get the correct URL in use.
$expected_compare = [];
foreach ($expected as $url => $label) {
$url = Url::fromUserInput($url)->toString();
$url = Url::fromUserInput($url);
$options = $url->getOptions();
if (isset($options['query']['destination'])) {
$options['query']['destination'] = Url::fromUserInput($options['query']['destination'])->toString();
}
$url->setOptions($options);
$url = $url->toString();
$expected_compare[$url] = $label;
}
$this->assertSame(array_keys($expected_compare), $urls, $text);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment