diff --git a/core/includes/menu.inc b/core/includes/menu.inc index d578499..56566e1 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -40,10 +40,6 @@ function template_preprocess_menu_local_task(&$variables) { $active = SafeMarkup::format('@label', array('@label' => t('(active tab)'))); $link_text = t('@local-task-title@active', array('@local-task-title' => $link_text, '@active' => $active)); } - else { - // @todo Remove this once https://www.drupal.org/node/2338081 is fixed. - $link_text = SafeMarkup::checkPlain($link_text); - } $link['localized_options']['set_active_class'] = TRUE; diff --git a/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php b/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php index 4fe7416..19671fe 100644 --- a/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php +++ b/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php @@ -7,7 +7,7 @@ namespace Drupal\Core\Menu; -use Drupal\Core\Plugin\PluginBase; +use Drupal\Component\Plugin\PluginBase; use Symfony\Component\HttpFoundation\Request; /** @@ -17,21 +17,10 @@ class ContextualLinkDefault extends PluginBase implements ContextualLinkInterfac /** * {@inheritdoc} - * - * @todo: It might be helpful at some point to move this getTitle logic into - * a trait. */ public function getTitle(Request $request = NULL) { - $options = array(); - if (!empty($this->pluginDefinition['title_context'])) { - $options['context'] = $this->pluginDefinition['title_context']; - } - $args = array(); - if (isset($this->pluginDefinition['title_arguments']) && $title_arguments = $this->pluginDefinition['title_arguments']) { - $args = (array) $title_arguments; - } - - return $this->t($this->pluginDefinition['title'], $args, $options); + // The title from YAML file discovery may be a TranslationWrapper object. + return (string) $this->pluginDefinition['title']; } /** diff --git a/core/lib/Drupal/Core/Menu/ContextualLinkManager.php b/core/lib/Drupal/Core/Menu/ContextualLinkManager.php index 3482adc..25421d7 100644 --- a/core/lib/Drupal/Core/Menu/ContextualLinkManager.php +++ b/core/lib/Drupal/Core/Menu/ContextualLinkManager.php @@ -118,8 +118,9 @@ public function __construct(ControllerResolverInterface $controller_resolver, Mo */ protected function getDiscovery() { if (!isset($this->discovery)) { - $this->discovery = new YamlDiscovery('links.contextual', $this->moduleHandler->getModuleDirectories()); - $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery); + $yaml_discovery = new YamlDiscovery('links.contextual', $this->moduleHandler->getModuleDirectories()); + $yaml_discovery->addTranslatableProperty('title', 'title_context'); + $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery); } return $this->discovery; } diff --git a/core/lib/Drupal/Core/Menu/LocalActionDefault.php b/core/lib/Drupal/Core/Menu/LocalActionDefault.php index b830a2f..b0109a9 100644 --- a/core/lib/Drupal/Core/Menu/LocalActionDefault.php +++ b/core/lib/Drupal/Core/Menu/LocalActionDefault.php @@ -7,8 +7,9 @@ namespace Drupal\Core\Menu; +use Drupal\Component\Plugin\PluginBase; +use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\RouteProviderInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -19,6 +20,8 @@ */ class LocalActionDefault extends PluginBase implements LocalActionInterface, ContainerFactoryPluginInterface { + use DependencySerializationTrait; + /** * The route provider to load routes by name. * @@ -68,15 +71,8 @@ public function getRouteName() { */ public function getTitle(Request $request = NULL) { // Subclasses may pull in the request or specific attributes as parameters. - $options = array(); - if (!empty($this->pluginDefinition['title_context'])) { - $options['context'] = $this->pluginDefinition['title_context']; - } - $args = array(); - if (isset($this->pluginDefinition['title_arguments']) && $title_arguments = $this->pluginDefinition['title_arguments']) { - $args = (array) $title_arguments; - } - return $this->t($this->pluginDefinition['title'], $args, $options); + // The title from YAML file discovery may be a TranslationWrapper object. + return (string) $this->pluginDefinition['title']; } /** diff --git a/core/lib/Drupal/Core/Menu/LocalActionManager.php b/core/lib/Drupal/Core/Menu/LocalActionManager.php index db6acef..c387b15 100644 --- a/core/lib/Drupal/Core/Menu/LocalActionManager.php +++ b/core/lib/Drupal/Core/Menu/LocalActionManager.php @@ -142,8 +142,9 @@ public function __construct(ControllerResolverInterface $controller_resolver, Re */ protected function getDiscovery() { if (!isset($this->discovery)) { - $this->discovery = new YamlDiscovery('links.action', $this->moduleHandler->getModuleDirectories()); - $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery); + $yaml_discovery = new YamlDiscovery('links.action', $this->moduleHandler->getModuleDirectories()); + $yaml_discovery->addTranslatableProperty('title', 'title_context'); + $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery); } return $this->discovery; } diff --git a/core/lib/Drupal/Core/Menu/LocalTaskDefault.php b/core/lib/Drupal/Core/Menu/LocalTaskDefault.php index ecef19c..6a09ffa 100644 --- a/core/lib/Drupal/Core/Menu/LocalTaskDefault.php +++ b/core/lib/Drupal/Core/Menu/LocalTaskDefault.php @@ -7,7 +7,8 @@ namespace Drupal\Core\Menu; -use Drupal\Core\Plugin\PluginBase; +use Drupal\Component\Plugin\PluginBase; +use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Routing\RouteMatchInterface; use Symfony\Component\HttpFoundation\Request; @@ -16,6 +17,8 @@ */ class LocalTaskDefault extends PluginBase implements LocalTaskInterface { + use DependencySerializationTrait; + /** * The route provider to load routes by name. * @@ -75,16 +78,8 @@ public function getRouteParameters(RouteMatchInterface $route_match) { * {@inheritdoc} */ public function getTitle(Request $request = NULL) { - // Subclasses may pull in the request or specific attributes as parameters. - $options = array(); - if (!empty($this->pluginDefinition['title_context'])) { - $options['context'] = $this->pluginDefinition['title_context']; - } - $args = array(); - if (isset($this->pluginDefinition['title_arguments']) && $title_arguments = $this->pluginDefinition['title_arguments']) { - $args = (array) $title_arguments; - } - return $this->t($this->pluginDefinition['title'], $args, $options); + // The title from YAML file discovery may be a TranslationWrapper object. + return (string) $this->pluginDefinition['title']; } /** diff --git a/core/lib/Drupal/Core/Menu/LocalTaskManager.php b/core/lib/Drupal/Core/Menu/LocalTaskManager.php index 70b1102..5b12ac8 100644 --- a/core/lib/Drupal/Core/Menu/LocalTaskManager.php +++ b/core/lib/Drupal/Core/Menu/LocalTaskManager.php @@ -142,8 +142,9 @@ public function __construct(ControllerResolverInterface $controller_resolver, Re */ protected function getDiscovery() { if (!isset($this->discovery)) { - $this->discovery = new YamlDiscovery('links.task', $this->moduleHandler->getModuleDirectories()); - $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery); + $yaml_discovery = new YamlDiscovery('links.task', $this->moduleHandler->getModuleDirectories()); + $yaml_discovery->addTranslatableProperty('title', 'title_context'); + $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery); } return $this->discovery; } diff --git a/core/lib/Drupal/Core/Plugin/Discovery/YamlDiscovery.php b/core/lib/Drupal/Core/Plugin/Discovery/YamlDiscovery.php index 0b7d10f6..d0d35ba 100644 --- a/core/lib/Drupal/Core/Plugin/Discovery/YamlDiscovery.php +++ b/core/lib/Drupal/Core/Plugin/Discovery/YamlDiscovery.php @@ -10,9 +10,15 @@ use Drupal\Component\Plugin\Discovery\DiscoveryInterface; use Drupal\Component\Discovery\YamlDiscovery as ComponentYamlDiscovery; use Drupal\Component\Plugin\Discovery\DiscoveryTrait; +use Drupal\Core\StringTranslation\TranslationWrapper; /** * Allows YAML files to define plugin definitions. + * + * For translatable values of plugin defintions, like title, you can specify + * $translatable_properties as part of the constructor. This is used in order + * to use a translation wrapper to 100% indicate that something is safe, as it + * was written in the YAML file and did not come from plugin definition. */ class YamlDiscovery implements DiscoveryInterface { @@ -26,6 +32,15 @@ class YamlDiscovery implements DiscoveryInterface { protected $discovery; /** + * Contains an array of translatable properties passed along to t(). + * + * @see \Drupal\Core\Plugin\Discovery\YamlDiscovery::__construct + * + * @var array + */ + protected $translatableProperties = []; + + /** * Construct a YamlDiscovery object. * * @param string $name @@ -39,6 +54,23 @@ function __construct($name, array $directories) { } /** + * Set one or more of the YAML values as being translatable. + * + * @param $value_key + * The key corresponding to the value in the YAML that contains a + * translatable string. + * @param string $context_key + * (Optional) the translation context for the value specified by the + * $value_key. + * + * @return $this + */ + public function addTranslatableProperty($value_key, $context_key = '') { + $this->translatableProperties[$value_key] = $context_key; + return $this; + } + + /** * {@inheritdoc} */ public function getDefinitions() { @@ -48,6 +80,20 @@ public function getDefinitions() { $definitions = array(); foreach ($plugins as $provider => $list) { foreach ($list as $id => $definition) { + // Add translation wrappers. + foreach ($this->translatableProperties as $property => $context_key) { + if (isset($definition[$property])) { + $options = []; + // Move the t() context from the definition to the translation + // wrapper. + if ($context_key && isset($definition[$context_key])) { + $options['context'] = $definition[$context_key]; + unset($definition[$context_key]); + } + $definition[$property] = new TranslationWrapper($definition[$property], [], $options); + } + } + // Add ID and provider. $definitions[$id] = $definition + array( 'provider' => $provider, 'id' => $id, diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php index 3a36db5..6bf591a 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php @@ -85,6 +85,16 @@ public function getOption($name) { } /** + * Gets all options from this translation wrapper. + * + * @return mixed[] + * The array of options. + */ + public function getOptions() { + return $this->options; + } + + /** * Implements the magic __toString() method. */ public function __toString() { diff --git a/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php b/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php index 0b8f6da..bbbfa8e 100644 --- a/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php +++ b/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php @@ -10,12 +10,14 @@ use Drupal\comment\CommentStorageInterface; use Drupal\Core\Menu\LocalTaskDefault; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a local task that shows the amount of unapproved comments. */ class UnapprovedComments extends LocalTaskDefault implements ContainerFactoryPluginInterface { + use StringTranslationTrait; /** * The comment storage service. @@ -57,7 +59,7 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ public function getTitle() { - return t('Unapproved comments (@count)', array('@count' => $this->commentStorage->getUnapprovedCount())); + return $this->t('Unapproved comments (@count)', array('@count' => $this->commentStorage->getUnapprovedCount())); } } diff --git a/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php b/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php index 98ffb0e..c8989fc 100644 --- a/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php +++ b/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php @@ -9,11 +9,13 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Menu\ContextualLinkDefault; +use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Defines a contextual link plugin with a dynamic title. */ class ConfigTranslationContextualLink extends ContextualLinkDefault { + use StringTranslationTrait; /** * The mapper plugin discovery service. @@ -26,17 +28,12 @@ class ConfigTranslationContextualLink extends ContextualLinkDefault { * {@inheritdoc} */ public function getTitle() { - $options = array(); - if (!empty($this->pluginDefinition['title_context'])) { - $options['context'] = $this->pluginDefinition['title_context']; - } - - // Take custom 'config_translation_plugin_id' plugin definition key to - // retrieve title. We need to retrieve a runtime title (as opposed to - // storing the title on the plugin definition for the link) because - // it contains translated parts that we need in the runtime language. + // Use the custom 'config_translation_plugin_id' plugin definition key to + // retrieve the title. We need to retrieve a runtime title (as opposed to + // storing the title on the plugin definition for the link) because it + // contains translated parts that we need in the runtime language. $type_name = Unicode::strtolower($this->mapperManager()->createInstance($this->pluginDefinition['config_translation_plugin_id'])->getTypeLabel()); - return $this->t($this->pluginDefinition['title'], array('@type_name' => $type_name), $options); + return $this->t('Translate @type_name', array('@type_name' => $type_name)); } /** diff --git a/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php b/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php index 46a2ec2..496ed96 100644 --- a/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php +++ b/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php @@ -9,11 +9,13 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Menu\LocalTaskDefault; +use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Defines a local task plugin with a dynamic title. */ class ConfigTranslationLocalTask extends LocalTaskDefault { + use StringTranslationTrait; /** * The mapper plugin discovery service. @@ -26,17 +28,12 @@ class ConfigTranslationLocalTask extends LocalTaskDefault { * {@inheritdoc} */ public function getTitle() { - $options = array(); - if (!empty($this->pluginDefinition['title_context'])) { - $options['context'] = $this->pluginDefinition['title_context']; - } - // Take custom 'config_translation_plugin_id' plugin definition key to // retrieve title. We need to retrieve a runtime title (as opposed to // storing the title on the plugin definition for the link) because // it contains translated parts that we need in the runtime language. $type_name = Unicode::strtolower($this->mapperManager()->createInstance($this->pluginDefinition['config_translation_plugin_id'])->getTypeLabel()); - return $this->t($this->pluginDefinition['title'], array('@type_name' => $type_name), $options); + return $this->t('Translate @type_name', array('@type_name' => $type_name)); } /** diff --git a/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php index 8846ab9..b8ed11c 100644 --- a/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php +++ b/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php @@ -8,6 +8,7 @@ namespace Drupal\contextual\Tests; use Drupal\Component\Serialization\Json; +use Drupal\Core\Url; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\simpletest\WebTestBase; use Drupal\Core\Template\Attribute; @@ -46,7 +47,7 @@ class ContextualDynamicContextTest extends WebTestBase { * * @var array */ - public static $modules = array('contextual', 'node', 'views', 'views_ui', 'language'); + public static $modules = array('contextual', 'node', 'views', 'views_ui', 'language', 'menu_test'); protected function setUp() { parent::setUp(); @@ -137,6 +138,11 @@ function testDifferentPermissions() { $id = 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=it'; $this->drupalGet('node', ['language' => ConfigurableLanguage::createFromLangcode('it')]); $this->assertContextualLinkPlaceHolder($id); + + // Get a page where contextual links are directly rendered. + $this->drupalGet(Url::fromRoute('menu_test.contextual_test')); + $this->assertEscaped(""); + $this->assertLink('Edit menu - contextual'); } /** diff --git a/core/modules/system/src/Tests/Menu/LocalActionTest.php b/core/modules/system/src/Tests/Menu/LocalActionTest.php index c049198..620aed5 100644 --- a/core/modules/system/src/Tests/Menu/LocalActionTest.php +++ b/core/modules/system/src/Tests/Menu/LocalActionTest.php @@ -30,6 +30,8 @@ public function testLocalAction() { // Ensure that both menu and route based actions are shown. $this->assertLocalAction([ [Url::fromRoute('menu_test.local_action4'), 'My dynamic-title action'], + [Url::fromRoute('menu_test.local_action4'), htmlspecialchars("", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')], + [Url::fromRoute('menu_test.local_action4'), htmlspecialchars("", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')], [Url::fromRoute('menu_test.local_action2'), 'My hook_menu action'], [Url::fromRoute('menu_test.local_action3'), 'My YAML discovery action'], [Url::fromRoute('menu_test.local_action5'), 'Title override'], @@ -50,7 +52,9 @@ protected function assertLocalAction(array $actions) { foreach ($actions as $action) { /** @var \Drupal\Core\Url $url */ list($url, $title) = $action; - $this->assertEqual((string) $elements[$index], $title); + // SimpleXML gives us the unescaped text, not the actual escaped markup, + // so use a pattern instead to check the raw content. + $this->assertPattern('@]*class="[^"]*button-action[^"]*"[^>]*>' . preg_quote($title, '@') . 'assertEqual($elements[$index]['href'], $url->toString()); $index++; } diff --git a/core/modules/system/src/Tests/Menu/LocalTasksTest.php b/core/modules/system/src/Tests/Menu/LocalTasksTest.php index b7824ef..79dc5e4 100644 --- a/core/modules/system/src/Tests/Menu/LocalTasksTest.php +++ b/core/modules/system/src/Tests/Menu/LocalTasksTest.php @@ -48,17 +48,51 @@ protected function assertLocalTasks(array $routes, $level = 0) { } /** + * Ensures that some local task appears. + * + * @param string $title + * The expected title. + * + * @return bool + * TRUE if the local task exists on the page. + */ + protected function assertLocalTaskAppers($title) { + // SimpleXML gives us the unescaped text, not the actual escaped markup, + // so use a pattern instead to check the raw content. + return $this->assertPattern('@]*>' . preg_quote($title, '@') . '@'); + } + + /** * Tests the plugin based local tasks. */ public function testPluginLocalTask() { + // Verify local tasks defined in the hook. + $this->drupalGet(Url::fromRoute('menu_test.tasks_default')); + $this->assertLocalTasks([ + ['menu_test.tasks_default', []], + ['menu_test.router_test1', ['bar' => 'unsafe']], + ['menu_test.router_test1', ['bar' => '1']], + ['menu_test.router_test2', ['bar' => '2']], + ]); + + // Verify that script tags are escaped on output. + $title = htmlspecialchars("Task 1 ", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $this->assertLocalTaskAppers($title); + $title = htmlspecialchars("", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $this->assertLocalTaskAppers($title); + // Verify that local tasks appear as defined in the router. $this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_view')); $this->assertLocalTasks([ ['menu_test.local_task_test_tasks_view', []], ['menu_test.local_task_test_tasks_edit', []], ['menu_test.local_task_test_tasks_settings', []], + ['menu_test.local_task_test_tasks_settings_dynamic', []], ]); + $title = htmlspecialchars("", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $this->assertLocalTaskAppers($title); + // Ensure the view tab is active. $result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]/a'); $this->assertEqual(1, count($result), 'There is just a single active tab.'); diff --git a/core/modules/system/src/Tests/Menu/MenuRouterTest.php b/core/modules/system/src/Tests/Menu/MenuRouterTest.php index ef78c7b..7fd809d 100644 --- a/core/modules/system/src/Tests/Menu/MenuRouterTest.php +++ b/core/modules/system/src/Tests/Menu/MenuRouterTest.php @@ -72,6 +72,8 @@ protected function doTestHookMenuIntegration() { // Confirm local task links are displayed. $this->assertLink('Local task A'); $this->assertLink('Local task B'); + $this->assertNoLink('Local task C'); + $this->assertEscaped("", ENT_QUOTES, 'UTF-8'); // Confirm correct local task href. $this->assertLinkByHref(Url::fromRoute('menu_test.router_test1', ['bar' => $machine_name])->toString()); $this->assertLinkByHref(Url::fromRoute('menu_test.router_test2', ['bar' => $machine_name])->toString()); diff --git a/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml b/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml index 24c92a2..c752990 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml +++ b/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml @@ -26,6 +26,22 @@ menu_test.local_action4: appears_on: - menu_test.local_action1 +menu_test.local_action_derivative: + route_name: menu_test.local_action4 + weight: -20 + deriver: Drupal\menu_test\Plugin\Derivative\LocalActionTest + class: Drupal\Core\Menu\LocalActionDefault + appears_on: + - menu_test.local_action1 + +menu_test.local_action6: + route_name: menu_test.local_action4 + title: 'Dynamic local action with user input' + weight: -15 + class: '\Drupal\menu_test\Plugin\Menu\LocalAction\TestLocalAction5' + appears_on: + - menu_test.local_action1 + menu_test.hidden_menu_add: route_name: menu_test.hidden_menu_add title: 'Add menu' diff --git a/core/modules/system/tests/modules/menu_test/menu_test.links.contextual.yml b/core/modules/system/tests/modules/menu_test/menu_test.links.contextual.yml index b2d8e47..9a8d8ad 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.links.contextual.yml +++ b/core/modules/system/tests/modules/menu_test/menu_test.links.contextual.yml @@ -1,12 +1,13 @@ -menu_test.hidden_manage: - title: 'List links' +menu_test.contextual_hidden_manage: + title: 'List links - contextual' group: menu_test_menu - route_name: menu_test.hidden_manage + route_name: menu_test.contextual_hidden_manage + class: '\Drupal\menu_test\Plugin\Menu\ContextualLink\TestContextualLink' -menu_test.hidden_manage_edit: - title: 'Edit menu' +menu_test.contextual_hidden_manage_edit: + title: 'Edit menu - contextual' group: menu_test_menu - route_name: menu_test.hidden_manage_edit + route_name: menu_test.contextual_hidden_manage_edit menu_test.hidden_block_configure: title: 'Configure block' diff --git a/core/modules/system/tests/modules/menu_test/menu_test.links.task.yml b/core/modules/system/tests/modules/menu_test/menu_test.links.task.yml index c8cd284..cda0f5a 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.links.task.yml +++ b/core/modules/system/tests/modules/menu_test/menu_test.links.task.yml @@ -10,6 +10,10 @@ menu_test.local_task_test_tasks_settings: route_name: menu_test.local_task_test_tasks_settings title: Settings base_route: menu_test.local_task_test_tasks_view +menu_test.local_task_test_tasks_settings_dynamic: + route_name: menu_test.local_task_test_tasks_settings_dynamic + base_route: menu_test.local_task_test_tasks_view + class: \Drupal\menu_test\Plugin\Menu\LocalTask\TestTaskWithUserInput menu_test.local_task_test_tasks_settings_sub1: route_name: menu_test.local_task_test_tasks_settings_sub1 title: sub1 @@ -53,6 +57,11 @@ menu_test.tasks_default_tab: route_name: menu_test.tasks_default title: 'View' base_route: menu_test.tasks_default +menu_test.tasks_default_derived: + route_name: menu_test.router_test1 + title: 'Derived' + base_route: menu_test.tasks_default + deriver: '\Drupal\menu_test\Plugin\Derivative\LocalTaskTestWithUnsafeTitle' menu_test.tasks_tasks_tab: route_name: menu_test.tasks_tasks @@ -82,3 +91,8 @@ menu_test.router_test3: route_name: menu_test.router_test3 title: 'Local task C' base_route: menu_test.router_test1 + +menu_test.router_test4: + route_name: menu_test.router_test4 + base_route: menu_test.router_test1 + class: \Drupal\menu_test\Plugin\Menu\LocalTask\TestTaskWithUserInput diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module index ec916cb..429b357 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.module +++ b/core/modules/system/tests/modules/menu_test/menu_test.module @@ -5,6 +5,8 @@ * Module that implements various hooks for menu tests. */ +use Drupal\Core\Url; + /** * Implements hook_menu_links_discovered_alter(). */ @@ -26,20 +28,14 @@ function menu_test_menu_links_discovered_alter(&$links) { /** * Implements hook_menu_local_tasks(). - * - * If the menu_test.settings configuration 'tasks.add' has been set, adds - * several local tasks to menu-test/tasks. */ function menu_test_menu_local_tasks(&$data, $route_name) { - if (!\Drupal::config('menu_test.settings')->get('tasks.add')) { - return; - } - if (in_array($route_name, array('menu_test.tasks_default', 'menu_test.tasks_empty', 'menu_test.tasks_tasks'))) { + if (in_array($route_name, array('menu_test.tasks_default'))) { $data['tabs'][0]['foo'] = array( '#theme' => 'menu_local_task', '#link' => array( - 'title' => 'Task 1', - 'href' => 'task/foo', + 'title' => "Task 1 ", + 'url' => Url::fromRoute('menu_test.router_test1', array('bar' => '1')), ), '#weight' => 10, ); @@ -47,7 +43,7 @@ function menu_test_menu_local_tasks(&$data, $route_name) { '#theme' => 'menu_local_task', '#link' => array( 'title' => 'Task 2', - 'href' => 'task/bar', + 'url' => Url::fromRoute('menu_test.router_test2', array('bar' => '2')), ), '#weight' => 20, ); diff --git a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml index 33a337b..b69d722 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml +++ b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml @@ -67,6 +67,13 @@ menu_test.router_test3: requirements: _access: 'FALSE' +menu_test.router_test4: + path: '/foo/{bar}/d' + defaults: + _controller: '\Drupal\menu_test\TestControllers::test2' + requirements: + _access: 'TRUE' + menu_test.local_action1: path: '/menu-test-local-action' defaults: @@ -102,6 +109,27 @@ menu_test.local_action5: requirements: _access: 'TRUE' +menu_test.contextual_test: + path: '/menu-test-contextual/default' + defaults: + _controller: '\Drupal\menu_test\TestControllers::testContextual' + requirements: + _access: 'TRUE' + +menu_test.contextual_hidden_manage: + path: '/menu-test-contextual/{bar}' + defaults: + _controller: '\Drupal\menu_test\TestControllers::test1' + requirements: + _access: 'TRUE' + +menu_test.contextual_hidden_manage_edit: + path: '/menu-test-contextual/{bar}/edit' + defaults: + _controller: '\Drupal\menu_test\TestControllers::test2' + requirements: + _access: 'TRUE' + menu_test.local_task_test_tasks: path: '/menu-local-task-test/tasks' defaults: @@ -109,6 +137,13 @@ menu_test.local_task_test_tasks: requirements: _access: 'TRUE' +menu_test.tasks_default: + path: '/menu-local-task-test/default' + defaults: + _controller: '\Drupal\menu_test\TestControllers::test1' + requirements: + _access: 'TRUE' + menu_test.local_task_test_tasks_tasks: path: '/menu-local-task-test/tasks/tasks' defaults: @@ -137,6 +172,13 @@ menu_test.local_task_test_tasks_settings: requirements: _access: 'TRUE' +menu_test.local_task_test_tasks_settings_dynamic: + path: '/menu-local-task-test/tasks/settings-dynamic' + defaults: + _controller: '\Drupal\menu_test\TestControllers::test1' + requirements: + _access: 'TRUE' + menu_test.local_task_test_tasks_settings_sub1: path: '/menu-local-task-test/tasks/settings/sub1' defaults: diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Derivative/LocalActionTest.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Derivative/LocalActionTest.php new file mode 100644 index 0000000..729519a --- /dev/null +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Derivative/LocalActionTest.php @@ -0,0 +1,30 @@ +derivatives['example'] = $base_plugin_definition + [ + 'title' => "", + ]; + + return $this->derivatives; + } + +} diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Derivative/LocalTaskTestWithUnsafeTitle.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Derivative/LocalTaskTestWithUnsafeTitle.php new file mode 100644 index 0000000..af6ff91 --- /dev/null +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Derivative/LocalTaskTestWithUnsafeTitle.php @@ -0,0 +1,31 @@ +derivatives['unsafe'] = [ + 'title' => "", + 'route_parameters' => ['bar' => 'unsafe'], + ] + $base_plugin_definition; + + return $this->derivatives; + } + +} diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/ContextualLink/TestContextualLink.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/ContextualLink/TestContextualLink.php new file mode 100644 index 0000000..fde09e2 --- /dev/null +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/ContextualLink/TestContextualLink.php @@ -0,0 +1,24 @@ +alert('Welcome to the jungle!')"; + } + +} diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php index 3f9a44e..a40af0c 100644 --- a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php @@ -8,12 +8,15 @@ namespace Drupal\menu_test\Plugin\Menu\LocalAction; use Drupal\Core\Menu\LocalActionDefault; +use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Defines a local action plugin with a dynamic title. */ class TestLocalAction4 extends LocalActionDefault { + use StringTranslationTrait; + /** * {@inheritdoc} */ diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction5.php similarity index 57% copy from core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php copy to core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction5.php index 3f9a44e..c39f1d4 100644 --- a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction5.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\menu_test\Plugin\Menu\LocalAction\TestLocalAction4. + * Contains \Drupal\menu_test\Plugin\Menu\LocalAction\TestLocalAction5. */ namespace Drupal\menu_test\Plugin\Menu\LocalAction; @@ -10,15 +10,15 @@ use Drupal\Core\Menu\LocalActionDefault; /** - * Defines a local action plugin with a dynamic title. + * Defines a local action plugin with a dynamic title from user input. */ -class TestLocalAction4 extends LocalActionDefault { +class TestLocalAction5 extends LocalActionDefault { /** * {@inheritdoc} */ public function getTitle() { - return $this->t('My @arg action', array('@arg' => 'dynamic-title')); + return ""; } } diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTaskWithUserInput.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTaskWithUserInput.php new file mode 100644 index 0000000..6ef6102 --- /dev/null +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTaskWithUserInput.php @@ -0,0 +1,22 @@ +alert('Welcome to the jungle!')"; + } + +} diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTasksSettingsSub1.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTasksSettingsSub1.php index ee5d594..67f0427 100644 --- a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTasksSettingsSub1.php +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTasksSettingsSub1.php @@ -8,9 +8,12 @@ namespace Drupal\menu_test\Plugin\Menu\LocalTask; use Drupal\Core\Menu\LocalTaskDefault; +use Drupal\Core\StringTranslation\StringTranslationTrait; class TestTasksSettingsSub1 extends LocalTaskDefault { + use StringTranslationTrait; + /** * {@inheritdoc} */ diff --git a/core/modules/system/tests/modules/menu_test/src/TestControllers.php b/core/modules/system/tests/modules/menu_test/src/TestControllers.php index 836415c..f3ff8d2 100644 --- a/core/modules/system/tests/modules/menu_test/src/TestControllers.php +++ b/core/modules/system/tests/modules/menu_test/src/TestControllers.php @@ -61,4 +61,20 @@ public function testDefaults($placeholder = NULL) { } } + /** + * Prints out test data with contextual links. + */ + public function testContextual() { + return [ + '#markup' => 'testContextual', + 'stuff' => [ + '#type' => 'contextual_links', + '#contextual_links' => [ + 'menu_test_menu' => [ + 'route_parameters' => ['bar' => 1], + ] + ] + ] + ]; + } } diff --git a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php index e3825ca..ac92088 100644 --- a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\Core\Menu; use Drupal\Core\Menu\ContextualLinkDefault; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\Tests\UnitTestCase; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; @@ -63,17 +64,18 @@ protected function setUp() { protected function setupContextualLinkDefault() { $this->contextualLinkDefault = new ContextualLinkDefault($this->config, $this->pluginId, $this->pluginDefinition); - $this->contextualLinkDefault->setStringTranslation($this->stringTranslation); } /** * @covers ::getTitle */ - public function testGetTitle($title = 'Example') { - $this->pluginDefinition['title'] = $title; + public function testGetTitle() { + $title = 'Example'; + $this->pluginDefinition['title'] = (new TranslationWrapper($title)) + ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) ->method('translate') - ->with($this->pluginDefinition['title'], array(), array()) + ->with($title, array(), array()) ->will($this->returnValue('Example translated')); $this->setupContextualLinkDefault(); @@ -84,11 +86,12 @@ public function testGetTitle($title = 'Example') { * @covers ::getTitle */ public function testGetTitleWithContext() { - $this->pluginDefinition['title'] = 'Example'; - $this->pluginDefinition['title_context'] = 'context'; + $title = 'Example'; + $this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context'))) + ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) ->method('translate') - ->with($this->pluginDefinition['title'], array(), array('context' => $this->pluginDefinition['title_context'])) + ->with($title, array(), array('context' => 'context')) ->will($this->returnValue('Example translated with context')); $this->setupContextualLinkDefault(); @@ -99,11 +102,12 @@ public function testGetTitleWithContext() { * @covers ::getTitle */ public function testGetTitleWithTitleArguments() { - $this->pluginDefinition['title'] = 'Example @test'; - $this->pluginDefinition['title_arguments'] = array('@test' => 'value'); + $title = 'Example @test'; + $this->pluginDefinition['title'] = (new TranslationWrapper($title, array('@test' => 'value'))) + ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) ->method('translate') - ->with($this->pluginDefinition['title'], $this->arrayHasKey('@test'), array()) + ->with($title, array('@test' => 'value'), array()) ->will($this->returnValue('Example value')); $this->setupContextualLinkDefault(); diff --git a/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php b/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php index dade254..02d4578 100644 --- a/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\Core\Menu; use Drupal\Core\Menu\LocalActionDefault; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\Tests\UnitTestCase; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; @@ -74,7 +75,6 @@ protected function setUp() { */ protected function setupLocalActionDefault() { $this->localActionDefault = new LocalActionDefault($this->config, $this->pluginId, $this->pluginDefinition, $this->routeProvider); - $this->localActionDefault->setStringTranslation($this->stringTranslation); } /** @@ -83,10 +83,11 @@ protected function setupLocalActionDefault() { * @see \Drupal\Core\Menu\LocalTaskDefault::getTitle() */ public function testGetTitle() { - $this->pluginDefinition['title'] = 'Example'; + $this->pluginDefinition['title'] = (new TranslationWrapper('Example')) + ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) ->method('translate') - ->with($this->pluginDefinition['title'], array(), array()) + ->with('Example', array(), array()) ->will($this->returnValue('Example translated')); $this->setupLocalActionDefault(); @@ -99,11 +100,11 @@ public function testGetTitle() { * @see \Drupal\Core\Menu\LocalTaskDefault::getTitle() */ public function testGetTitleWithContext() { - $this->pluginDefinition['title'] = 'Example'; - $this->pluginDefinition['title_context'] = 'context'; + $this->pluginDefinition['title'] = (new TranslationWrapper('Example', array(), array('context' => 'context'))) + ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) ->method('translate') - ->with($this->pluginDefinition['title'], array(), array('context' => 'context')) + ->with('Example', array(), array('context' => 'context')) ->will($this->returnValue('Example translated with context')); $this->setupLocalActionDefault(); @@ -114,11 +115,11 @@ public function testGetTitleWithContext() { * Tests the getTitle method with title arguments. */ public function testGetTitleWithTitleArguments() { - $this->pluginDefinition['title'] = 'Example @test'; - $this->pluginDefinition['title_arguments'] = array('@test' => 'value'); + $this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value'))) + ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) ->method('translate') - ->with($this->pluginDefinition['title'], $this->arrayHasKey('@test'), array()) + ->with('Example @test', array('@test' => 'value'), array()) ->will($this->returnValue('Example value')); $this->setupLocalActionDefault(); diff --git a/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php b/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php index 573444b..6e393a8 100644 --- a/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php @@ -10,8 +10,8 @@ use Drupal\Core\Menu\LocalTaskDefault; use Drupal\Core\Routing\RouteMatch; use Drupal\Core\Routing\RouteProviderInterface; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\Tests\UnitTestCase; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Route; /** @@ -77,9 +77,7 @@ protected function setUp() { protected function setupLocalTaskDefault() { $this->localTaskBase = new TestLocalTaskDefault($this->config, $this->pluginId, $this->pluginDefinition); $this->localTaskBase - ->setRouteProvider($this->routeProvider) - ->setStringTranslation($this->stringTranslation); - + ->setRouteProvider($this->routeProvider); } /** @@ -234,11 +232,12 @@ public function testActive() { /** * @covers ::getTitle */ - public function testGetTitleWithoutContext() { - $this->pluginDefinition['title'] = 'Example'; + public function testGetTitle() { + $this->pluginDefinition['title'] = (new TranslationWrapper('Example')) + ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) ->method('translate') - ->with($this->pluginDefinition['title'], array(), array()) + ->with('Example', array(), array()) ->will($this->returnValue('Example translated')); $this->setupLocalTaskDefault(); @@ -249,11 +248,12 @@ public function testGetTitleWithoutContext() { * @covers ::getTitle */ public function testGetTitleWithContext() { - $this->pluginDefinition['title'] = 'Example'; - $this->pluginDefinition['title_context'] = 'context'; + $title = 'Example'; + $this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context'))) + ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) ->method('translate') - ->with($this->pluginDefinition['title'], array(), array('context' => 'context')) + ->with($title, array(), array('context' => 'context')) ->will($this->returnValue('Example translated with context')); $this->setupLocalTaskDefault(); @@ -264,16 +264,16 @@ public function testGetTitleWithContext() { * @covers ::getTitle */ public function testGetTitleWithTitleArguments() { - $this->pluginDefinition['title'] = 'Example @test'; - $this->pluginDefinition['title_arguments'] = array('@test' => 'value'); + $title = 'Example @test'; + $this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value'))) + ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) ->method('translate') - ->with($this->pluginDefinition['title'], $this->arrayHasKey('@test'), array()) + ->with($title, array('@test' => 'value'), array()) ->will($this->returnValue('Example value')); $this->setupLocalTaskDefault(); - $request = new Request(); - $this->assertEquals('Example value', $this->localTaskBase->getTitle($request)); + $this->assertEquals('Example value', $this->localTaskBase->getTitle()); } /** diff --git a/core/tests/Drupal/Tests/Core/Plugin/Discovery/YamlDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Plugin/Discovery/YamlDiscoveryTest.php index 3aa99e4..22aacc4 100644 --- a/core/tests/Drupal/Tests/Core/Plugin/Discovery/YamlDiscoveryTest.php +++ b/core/tests/Drupal/Tests/Core/Plugin/Discovery/YamlDiscoveryTest.php @@ -7,8 +7,10 @@ namespace Drupal\Tests\Core\Plugin\Discovery; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\Tests\UnitTestCase; use Drupal\Core\Plugin\Discovery\YamlDiscovery; +use org\bovigo\vfs\vfsStream; /** * @coversDefaultClass \Drupal\Core\Plugin\Discovery\YamlDiscovery @@ -71,6 +73,44 @@ public function testGetDefinitions() { } /** + * @covers ::getDefinitions + */ + public function testGetDefinitionsWithTranslatableDefinitions() { + vfsStream::setup('root'); + + $file_1 = <<<'EOS' +test_plugin: + title: test title +EOS; + $file_2 = <<<'EOS' +test_plugin2: + title: test title2 + title_context: 'test-context' +EOS; + vfsStream::create([ + 'test_1' => [ + 'test_1.test.yml' => $file_1, + ], + 'test_2' => [ + 'test_2.test.yml' => $file_2, + ]] + ); + + $discovery = new YamlDiscovery('test', ['test_1' => vfsStream::url('root/test_1'), 'test_2' => vfsStream::url('root/test_2')]); + $discovery->addTranslatableProperty('title', 'title_context'); + $definitions = $discovery->getDefinitions(); + + $this->assertCount(2, $definitions); + $plugin_1 = $definitions['test_plugin']; + $plugin_2 = $definitions['test_plugin2']; + + $this->assertInstanceOf(TranslationWrapper::class, $plugin_1['title']); + $this->assertEquals([], $plugin_1['title']->getOptions()); + $this->assertInstanceOf(TranslationWrapper::class, $plugin_2['title']); + $this->assertEquals(['context' => 'test-context'], $plugin_2['title']->getOptions()); + } + + /** * Tests the getDefinition() method. */ public function testGetDefinition() {