Issue #2338081 by pwolanin, dawehner, effulgentsia, kgoel, vijaycs85, mpdonadio, chx, brandon.holtsclaw, alexpott, Gábor Hojtsy, Fabianx, catch, YesCT, JvE: Local Tasks, Actions, and Contextual links mark strings from derivatives (or alter hooks) as safe and translated
parent
61603f58f6
commit
4b12bbf0db
|
|
@ -40,10 +40,6 @@ function template_preprocess_menu_local_task(&$variables) {
|
||||||
$active = SafeMarkup::format('<span class="visually-hidden">@label</span>', array('@label' => t('(active tab)')));
|
$active = SafeMarkup::format('<span class="visually-hidden">@label</span>', array('@label' => t('(active tab)')));
|
||||||
$link_text = t('@local-task-title@active', array('@local-task-title' => $link_text, '@active' => $active));
|
$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;
|
$link['localized_options']['set_active_class'] = TRUE;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
namespace Drupal\Core\Menu;
|
namespace Drupal\Core\Menu;
|
||||||
|
|
||||||
use Drupal\Core\Plugin\PluginBase;
|
use Drupal\Component\Plugin\PluginBase;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,21 +17,10 @@ class ContextualLinkDefault extends PluginBase implements ContextualLinkInterfac
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*
|
|
||||||
* @todo: It might be helpful at some point to move this getTitle logic into
|
|
||||||
* a trait.
|
|
||||||
*/
|
*/
|
||||||
public function getTitle(Request $request = NULL) {
|
public function getTitle(Request $request = NULL) {
|
||||||
$options = array();
|
// The title from YAML file discovery may be a TranslationWrapper object.
|
||||||
if (!empty($this->pluginDefinition['title_context'])) {
|
return (string) $this->pluginDefinition['title'];
|
||||||
$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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -118,8 +118,9 @@ class ContextualLinkManager extends DefaultPluginManager implements ContextualLi
|
||||||
*/
|
*/
|
||||||
protected function getDiscovery() {
|
protected function getDiscovery() {
|
||||||
if (!isset($this->discovery)) {
|
if (!isset($this->discovery)) {
|
||||||
$this->discovery = new YamlDiscovery('links.contextual', $this->moduleHandler->getModuleDirectories());
|
$yaml_discovery = new YamlDiscovery('links.contextual', $this->moduleHandler->getModuleDirectories());
|
||||||
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
|
$yaml_discovery->addTranslatableProperty('title', 'title_context');
|
||||||
|
$this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery);
|
||||||
}
|
}
|
||||||
return $this->discovery;
|
return $this->discovery;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@
|
||||||
|
|
||||||
namespace Drupal\Core\Menu;
|
namespace Drupal\Core\Menu;
|
||||||
|
|
||||||
|
use Drupal\Component\Plugin\PluginBase;
|
||||||
|
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
|
||||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||||
use Drupal\Core\Plugin\PluginBase;
|
|
||||||
use Drupal\Core\Routing\RouteMatchInterface;
|
use Drupal\Core\Routing\RouteMatchInterface;
|
||||||
use Drupal\Core\Routing\RouteProviderInterface;
|
use Drupal\Core\Routing\RouteProviderInterface;
|
||||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
|
@ -19,6 +20,8 @@ use Symfony\Component\HttpFoundation\Request;
|
||||||
*/
|
*/
|
||||||
class LocalActionDefault extends PluginBase implements LocalActionInterface, ContainerFactoryPluginInterface {
|
class LocalActionDefault extends PluginBase implements LocalActionInterface, ContainerFactoryPluginInterface {
|
||||||
|
|
||||||
|
use DependencySerializationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The route provider to load routes by name.
|
* The route provider to load routes by name.
|
||||||
*
|
*
|
||||||
|
|
@ -68,15 +71,8 @@ class LocalActionDefault extends PluginBase implements LocalActionInterface, Con
|
||||||
*/
|
*/
|
||||||
public function getTitle(Request $request = NULL) {
|
public function getTitle(Request $request = NULL) {
|
||||||
// Subclasses may pull in the request or specific attributes as parameters.
|
// Subclasses may pull in the request or specific attributes as parameters.
|
||||||
$options = array();
|
// The title from YAML file discovery may be a TranslationWrapper object.
|
||||||
if (!empty($this->pluginDefinition['title_context'])) {
|
return (string) $this->pluginDefinition['title'];
|
||||||
$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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,9 @@ class LocalActionManager extends DefaultPluginManager implements LocalActionMana
|
||||||
*/
|
*/
|
||||||
protected function getDiscovery() {
|
protected function getDiscovery() {
|
||||||
if (!isset($this->discovery)) {
|
if (!isset($this->discovery)) {
|
||||||
$this->discovery = new YamlDiscovery('links.action', $this->moduleHandler->getModuleDirectories());
|
$yaml_discovery = new YamlDiscovery('links.action', $this->moduleHandler->getModuleDirectories());
|
||||||
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
|
$yaml_discovery->addTranslatableProperty('title', 'title_context');
|
||||||
|
$this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery);
|
||||||
}
|
}
|
||||||
return $this->discovery;
|
return $this->discovery;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
|
|
||||||
namespace Drupal\Core\Menu;
|
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 Drupal\Core\Routing\RouteMatchInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
|
@ -16,6 +17,8 @@ use Symfony\Component\HttpFoundation\Request;
|
||||||
*/
|
*/
|
||||||
class LocalTaskDefault extends PluginBase implements LocalTaskInterface {
|
class LocalTaskDefault extends PluginBase implements LocalTaskInterface {
|
||||||
|
|
||||||
|
use DependencySerializationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The route provider to load routes by name.
|
* The route provider to load routes by name.
|
||||||
*
|
*
|
||||||
|
|
@ -75,16 +78,8 @@ class LocalTaskDefault extends PluginBase implements LocalTaskInterface {
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function getTitle(Request $request = NULL) {
|
public function getTitle(Request $request = NULL) {
|
||||||
// Subclasses may pull in the request or specific attributes as parameters.
|
// The title from YAML file discovery may be a TranslationWrapper object.
|
||||||
$options = array();
|
return (string) $this->pluginDefinition['title'];
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,9 @@ class LocalTaskManager extends DefaultPluginManager implements LocalTaskManagerI
|
||||||
*/
|
*/
|
||||||
protected function getDiscovery() {
|
protected function getDiscovery() {
|
||||||
if (!isset($this->discovery)) {
|
if (!isset($this->discovery)) {
|
||||||
$this->discovery = new YamlDiscovery('links.task', $this->moduleHandler->getModuleDirectories());
|
$yaml_discovery = new YamlDiscovery('links.task', $this->moduleHandler->getModuleDirectories());
|
||||||
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
|
$yaml_discovery->addTranslatableProperty('title', 'title_context');
|
||||||
|
$this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery);
|
||||||
}
|
}
|
||||||
return $this->discovery;
|
return $this->discovery;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,18 @@ namespace Drupal\Core\Plugin\Discovery;
|
||||||
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
|
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
|
||||||
use Drupal\Component\Discovery\YamlDiscovery as ComponentYamlDiscovery;
|
use Drupal\Component\Discovery\YamlDiscovery as ComponentYamlDiscovery;
|
||||||
use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
|
use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
|
||||||
|
use Drupal\Core\StringTranslation\TranslationWrapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows YAML files to define plugin definitions.
|
* Allows YAML files to define plugin definitions.
|
||||||
|
*
|
||||||
|
* If the value of a key (like title) in the definition is translatable then
|
||||||
|
* the addTranslatableProperty() method can be used to mark it as such and also
|
||||||
|
* to add translation context. Then
|
||||||
|
* \Drupal\Core\StringTranslation\TranslationWrapper will be used to translate
|
||||||
|
* the string and also to mark it safe. Only strings written in the YAML files
|
||||||
|
* should be marked as safe, strings coming from dynamic plugin definitions
|
||||||
|
* potentially containing user input should not.
|
||||||
*/
|
*/
|
||||||
class YamlDiscovery implements DiscoveryInterface {
|
class YamlDiscovery implements DiscoveryInterface {
|
||||||
|
|
||||||
|
|
@ -25,6 +34,15 @@ class YamlDiscovery implements DiscoveryInterface {
|
||||||
*/
|
*/
|
||||||
protected $discovery;
|
protected $discovery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains an array of translatable properties passed along to t().
|
||||||
|
*
|
||||||
|
* @see \Drupal\Core\Plugin\Discovery\YamlDiscovery::addTranslatableProperty()
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $translatableProperties = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a YamlDiscovery object.
|
* Construct a YamlDiscovery object.
|
||||||
*
|
*
|
||||||
|
|
@ -38,6 +56,23 @@ class YamlDiscovery implements DiscoveryInterface {
|
||||||
$this->discovery = new ComponentYamlDiscovery($name, $directories);
|
$this->discovery = new ComponentYamlDiscovery($name, $directories);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set one of the YAML values as being translatable.
|
||||||
|
*
|
||||||
|
* @param string $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}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
|
@ -48,6 +83,20 @@ class YamlDiscovery implements DiscoveryInterface {
|
||||||
$definitions = array();
|
$definitions = array();
|
||||||
foreach ($plugins as $provider => $list) {
|
foreach ($plugins as $provider => $list) {
|
||||||
foreach ($list as $id => $definition) {
|
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(
|
$definitions[$id] = $definition + array(
|
||||||
'provider' => $provider,
|
'provider' => $provider,
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,16 @@ class TranslationWrapper implements SafeStringInterface {
|
||||||
return isset($this->options[$name]) ? $this->options[$name] : '';
|
return isset($this->options[$name]) ? $this->options[$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.
|
* Implements the magic __toString() method.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,14 @@ namespace Drupal\comment\Plugin\Menu\LocalTask;
|
||||||
use Drupal\comment\CommentStorageInterface;
|
use Drupal\comment\CommentStorageInterface;
|
||||||
use Drupal\Core\Menu\LocalTaskDefault;
|
use Drupal\Core\Menu\LocalTaskDefault;
|
||||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||||
|
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a local task that shows the amount of unapproved comments.
|
* Provides a local task that shows the amount of unapproved comments.
|
||||||
*/
|
*/
|
||||||
class UnapprovedComments extends LocalTaskDefault implements ContainerFactoryPluginInterface {
|
class UnapprovedComments extends LocalTaskDefault implements ContainerFactoryPluginInterface {
|
||||||
|
use StringTranslationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The comment storage service.
|
* The comment storage service.
|
||||||
|
|
@ -57,7 +59,7 @@ class UnapprovedComments extends LocalTaskDefault implements ContainerFactoryPlu
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function getTitle() {
|
public function getTitle() {
|
||||||
return t('Unapproved comments (@count)', array('@count' => $this->commentStorage->getUnapprovedCount()));
|
return $this->t('Unapproved comments (@count)', array('@count' => $this->commentStorage->getUnapprovedCount()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
config_translation.contextual_links:
|
config_translation.contextual_links:
|
||||||
title: 'Translate @type_name'
|
|
||||||
deriver: 'Drupal\config_translation\Plugin\Derivative\ConfigTranslationContextualLinks'
|
deriver: 'Drupal\config_translation\Plugin\Derivative\ConfigTranslationContextualLinks'
|
||||||
weight: 100
|
weight: 100
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
config_translation.local_tasks:
|
config_translation.local_tasks:
|
||||||
title: 'Translate @type_name'
|
|
||||||
deriver: 'Drupal\config_translation\Plugin\Derivative\ConfigTranslationLocalTasks'
|
deriver: 'Drupal\config_translation\Plugin\Derivative\ConfigTranslationLocalTasks'
|
||||||
weight: 100
|
weight: 100
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,13 @@ namespace Drupal\config_translation\Plugin\Menu\ContextualLink;
|
||||||
|
|
||||||
use Drupal\Component\Utility\Unicode;
|
use Drupal\Component\Utility\Unicode;
|
||||||
use Drupal\Core\Menu\ContextualLinkDefault;
|
use Drupal\Core\Menu\ContextualLinkDefault;
|
||||||
|
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines a contextual link plugin with a dynamic title.
|
* Defines a contextual link plugin with a dynamic title.
|
||||||
*/
|
*/
|
||||||
class ConfigTranslationContextualLink extends ContextualLinkDefault {
|
class ConfigTranslationContextualLink extends ContextualLinkDefault {
|
||||||
|
use StringTranslationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The mapper plugin discovery service.
|
* The mapper plugin discovery service.
|
||||||
|
|
@ -26,17 +28,12 @@ class ConfigTranslationContextualLink extends ContextualLinkDefault {
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function getTitle() {
|
public function getTitle() {
|
||||||
$options = array();
|
// Use the custom 'config_translation_plugin_id' plugin definition key to
|
||||||
if (!empty($this->pluginDefinition['title_context'])) {
|
// retrieve the title. We need to retrieve a runtime title (as opposed to
|
||||||
$options['context'] = $this->pluginDefinition['title_context'];
|
// storing the title on the plugin definition for the link) because it
|
||||||
}
|
// contains translated parts that we need in the runtime language.
|
||||||
|
|
||||||
// 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());
|
$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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,13 @@ namespace Drupal\config_translation\Plugin\Menu\LocalTask;
|
||||||
|
|
||||||
use Drupal\Component\Utility\Unicode;
|
use Drupal\Component\Utility\Unicode;
|
||||||
use Drupal\Core\Menu\LocalTaskDefault;
|
use Drupal\Core\Menu\LocalTaskDefault;
|
||||||
|
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines a local task plugin with a dynamic title.
|
* Defines a local task plugin with a dynamic title.
|
||||||
*/
|
*/
|
||||||
class ConfigTranslationLocalTask extends LocalTaskDefault {
|
class ConfigTranslationLocalTask extends LocalTaskDefault {
|
||||||
|
use StringTranslationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The mapper plugin discovery service.
|
* The mapper plugin discovery service.
|
||||||
|
|
@ -26,17 +28,12 @@ class ConfigTranslationLocalTask extends LocalTaskDefault {
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function getTitle() {
|
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
|
// Take custom 'config_translation_plugin_id' plugin definition key to
|
||||||
// retrieve title. We need to retrieve a runtime title (as opposed to
|
// retrieve title. We need to retrieve a runtime title (as opposed to
|
||||||
// storing the title on the plugin definition for the link) because
|
// storing the title on the plugin definition for the link) because
|
||||||
// it contains translated parts that we need in the runtime language.
|
// 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());
|
$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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
namespace Drupal\contextual\Tests;
|
namespace Drupal\contextual\Tests;
|
||||||
|
|
||||||
use Drupal\Component\Serialization\Json;
|
use Drupal\Component\Serialization\Json;
|
||||||
|
use Drupal\Core\Url;
|
||||||
use Drupal\language\Entity\ConfigurableLanguage;
|
use Drupal\language\Entity\ConfigurableLanguage;
|
||||||
use Drupal\simpletest\WebTestBase;
|
use Drupal\simpletest\WebTestBase;
|
||||||
use Drupal\Core\Template\Attribute;
|
use Drupal\Core\Template\Attribute;
|
||||||
|
|
@ -46,7 +47,7 @@ class ContextualDynamicContextTest extends WebTestBase {
|
||||||
*
|
*
|
||||||
* @var array
|
* @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() {
|
protected function setUp() {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
@ -137,6 +138,11 @@ class ContextualDynamicContextTest extends WebTestBase {
|
||||||
$id = 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=it';
|
$id = 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=it';
|
||||||
$this->drupalGet('node', ['language' => ConfigurableLanguage::createFromLangcode('it')]);
|
$this->drupalGet('node', ['language' => ConfigurableLanguage::createFromLangcode('it')]);
|
||||||
$this->assertContextualLinkPlaceHolder($id);
|
$this->assertContextualLinkPlaceHolder($id);
|
||||||
|
|
||||||
|
// Get a page where contextual links are directly rendered.
|
||||||
|
$this->drupalGet(Url::fromRoute('menu_test.contextual_test'));
|
||||||
|
$this->assertEscaped("<script>alert('Welcome to the jungle!')</script>");
|
||||||
|
$this->assertLink('Edit menu - contextual');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ class LocalActionTest extends WebTestBase {
|
||||||
// Ensure that both menu and route based actions are shown.
|
// Ensure that both menu and route based actions are shown.
|
||||||
$this->assertLocalAction([
|
$this->assertLocalAction([
|
||||||
[Url::fromRoute('menu_test.local_action4'), 'My dynamic-title action'],
|
[Url::fromRoute('menu_test.local_action4'), 'My dynamic-title action'],
|
||||||
|
[Url::fromRoute('menu_test.local_action4'), htmlspecialchars("<script>alert('Welcome to the jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')],
|
||||||
|
[Url::fromRoute('menu_test.local_action4'), htmlspecialchars("<script>alert('Welcome to the derived jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')],
|
||||||
[Url::fromRoute('menu_test.local_action2'), 'My hook_menu action'],
|
[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_action3'), 'My YAML discovery action'],
|
||||||
[Url::fromRoute('menu_test.local_action5'), 'Title override'],
|
[Url::fromRoute('menu_test.local_action5'), 'Title override'],
|
||||||
|
|
@ -50,7 +52,11 @@ class LocalActionTest extends WebTestBase {
|
||||||
foreach ($actions as $action) {
|
foreach ($actions as $action) {
|
||||||
/** @var \Drupal\Core\Url $url */
|
/** @var \Drupal\Core\Url $url */
|
||||||
list($url, $title) = $action;
|
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 behaviour is a bug in libxml, see
|
||||||
|
// https://bugs.php.net/bug.php?id=49437.
|
||||||
|
$this->assertPattern('@<a [^>]*class="[^"]*button-action[^"]*"[^>]*>' . preg_quote($title, '@') . '</@');
|
||||||
$this->assertEqual($elements[$index]['href'], $url->toString());
|
$this->assertEqual($elements[$index]['href'], $url->toString());
|
||||||
$index++;
|
$index++;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,18 +47,54 @@ class LocalTasksTest extends WebTestBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
// This behaviour is a bug in libxml, see
|
||||||
|
// https://bugs.php.net/bug.php?id=49437.
|
||||||
|
return $this->assertPattern('@<a [^>]*>' . preg_quote($title, '@') . '</a>@');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests the plugin based local tasks.
|
* Tests the plugin based local tasks.
|
||||||
*/
|
*/
|
||||||
public function testPluginLocalTask() {
|
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 <script>alert('Welcome to the jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
$this->assertLocalTaskAppers($title);
|
||||||
|
$title = htmlspecialchars("<script>alert('Welcome to the derived jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
$this->assertLocalTaskAppers($title);
|
||||||
|
|
||||||
// Verify that local tasks appear as defined in the router.
|
// Verify that local tasks appear as defined in the router.
|
||||||
$this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_view'));
|
$this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_view'));
|
||||||
$this->assertLocalTasks([
|
$this->assertLocalTasks([
|
||||||
['menu_test.local_task_test_tasks_view', []],
|
['menu_test.local_task_test_tasks_view', []],
|
||||||
['menu_test.local_task_test_tasks_edit', []],
|
['menu_test.local_task_test_tasks_edit', []],
|
||||||
['menu_test.local_task_test_tasks_settings', []],
|
['menu_test.local_task_test_tasks_settings', []],
|
||||||
|
['menu_test.local_task_test_tasks_settings_dynamic', []],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$title = htmlspecialchars("<script>alert('Welcome to the jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
$this->assertLocalTaskAppers($title);
|
||||||
|
|
||||||
// Ensure the view tab is active.
|
// Ensure the view tab is active.
|
||||||
$result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]/a');
|
$result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]/a');
|
||||||
$this->assertEqual(1, count($result), 'There is just a single active tab.');
|
$this->assertEqual(1, count($result), 'There is just a single active tab.');
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ class MenuRouterTest extends WebTestBase {
|
||||||
// Confirm local task links are displayed.
|
// Confirm local task links are displayed.
|
||||||
$this->assertLink('Local task A');
|
$this->assertLink('Local task A');
|
||||||
$this->assertLink('Local task B');
|
$this->assertLink('Local task B');
|
||||||
|
$this->assertNoLink('Local task C');
|
||||||
|
$this->assertEscaped("<script>alert('Welcome to the jungle!')</script>", ENT_QUOTES, 'UTF-8');
|
||||||
// Confirm correct local task href.
|
// Confirm correct local task href.
|
||||||
$this->assertLinkByHref(Url::fromRoute('menu_test.router_test1', ['bar' => $machine_name])->toString());
|
$this->assertLinkByHref(Url::fromRoute('menu_test.router_test1', ['bar' => $machine_name])->toString());
|
||||||
$this->assertLinkByHref(Url::fromRoute('menu_test.router_test2', ['bar' => $machine_name])->toString());
|
$this->assertLinkByHref(Url::fromRoute('menu_test.router_test2', ['bar' => $machine_name])->toString());
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,22 @@ menu_test.local_action4:
|
||||||
appears_on:
|
appears_on:
|
||||||
- menu_test.local_action1
|
- 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:
|
menu_test.hidden_menu_add:
|
||||||
route_name: menu_test.hidden_menu_add
|
route_name: menu_test.hidden_menu_add
|
||||||
title: 'Add menu'
|
title: 'Add menu'
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
menu_test.hidden_manage:
|
menu_test.contextual_hidden_manage:
|
||||||
title: 'List links'
|
title: 'List links - contextual'
|
||||||
group: menu_test_menu
|
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:
|
menu_test.contextual_hidden_manage_edit:
|
||||||
title: 'Edit menu'
|
title: 'Edit menu - contextual'
|
||||||
group: menu_test_menu
|
group: menu_test_menu
|
||||||
route_name: menu_test.hidden_manage_edit
|
route_name: menu_test.contextual_hidden_manage_edit
|
||||||
|
|
||||||
menu_test.hidden_block_configure:
|
menu_test.hidden_block_configure:
|
||||||
title: 'Configure block'
|
title: 'Configure block'
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ menu_test.local_task_test_tasks_settings:
|
||||||
route_name: menu_test.local_task_test_tasks_settings
|
route_name: menu_test.local_task_test_tasks_settings
|
||||||
title: Settings
|
title: Settings
|
||||||
base_route: menu_test.local_task_test_tasks_view
|
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:
|
menu_test.local_task_test_tasks_settings_sub1:
|
||||||
route_name: menu_test.local_task_test_tasks_settings_sub1
|
route_name: menu_test.local_task_test_tasks_settings_sub1
|
||||||
title: sub1
|
title: sub1
|
||||||
|
|
@ -53,6 +57,11 @@ menu_test.tasks_default_tab:
|
||||||
route_name: menu_test.tasks_default
|
route_name: menu_test.tasks_default
|
||||||
title: 'View'
|
title: 'View'
|
||||||
base_route: menu_test.tasks_default
|
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:
|
menu_test.tasks_tasks_tab:
|
||||||
route_name: menu_test.tasks_tasks
|
route_name: menu_test.tasks_tasks
|
||||||
|
|
@ -82,3 +91,8 @@ menu_test.router_test3:
|
||||||
route_name: menu_test.router_test3
|
route_name: menu_test.router_test3
|
||||||
title: 'Local task C'
|
title: 'Local task C'
|
||||||
base_route: menu_test.router_test1
|
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
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
* Module that implements various hooks for menu tests.
|
* Module that implements various hooks for menu tests.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use Drupal\Core\Url;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements hook_menu_links_discovered_alter().
|
* Implements hook_menu_links_discovered_alter().
|
||||||
*/
|
*/
|
||||||
|
|
@ -26,20 +28,14 @@ function menu_test_menu_links_discovered_alter(&$links) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements hook_menu_local_tasks().
|
* 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) {
|
function menu_test_menu_local_tasks(&$data, $route_name) {
|
||||||
if (!\Drupal::config('menu_test.settings')->get('tasks.add')) {
|
if (in_array($route_name, array('menu_test.tasks_default'))) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (in_array($route_name, array('menu_test.tasks_default', 'menu_test.tasks_empty', 'menu_test.tasks_tasks'))) {
|
|
||||||
$data['tabs'][0]['foo'] = array(
|
$data['tabs'][0]['foo'] = array(
|
||||||
'#theme' => 'menu_local_task',
|
'#theme' => 'menu_local_task',
|
||||||
'#link' => array(
|
'#link' => array(
|
||||||
'title' => 'Task 1',
|
'title' => "Task 1 <script>alert('Welcome to the jungle!')</script>",
|
||||||
'href' => 'task/foo',
|
'url' => Url::fromRoute('menu_test.router_test1', array('bar' => '1')),
|
||||||
),
|
),
|
||||||
'#weight' => 10,
|
'#weight' => 10,
|
||||||
);
|
);
|
||||||
|
|
@ -47,7 +43,7 @@ function menu_test_menu_local_tasks(&$data, $route_name) {
|
||||||
'#theme' => 'menu_local_task',
|
'#theme' => 'menu_local_task',
|
||||||
'#link' => array(
|
'#link' => array(
|
||||||
'title' => 'Task 2',
|
'title' => 'Task 2',
|
||||||
'href' => 'task/bar',
|
'url' => Url::fromRoute('menu_test.router_test2', array('bar' => '2')),
|
||||||
),
|
),
|
||||||
'#weight' => 20,
|
'#weight' => 20,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,13 @@ menu_test.router_test3:
|
||||||
requirements:
|
requirements:
|
||||||
_access: 'FALSE'
|
_access: 'FALSE'
|
||||||
|
|
||||||
|
menu_test.router_test4:
|
||||||
|
path: '/foo/{bar}/d'
|
||||||
|
defaults:
|
||||||
|
_controller: '\Drupal\menu_test\TestControllers::test2'
|
||||||
|
requirements:
|
||||||
|
_access: 'TRUE'
|
||||||
|
|
||||||
menu_test.local_action1:
|
menu_test.local_action1:
|
||||||
path: '/menu-test-local-action'
|
path: '/menu-test-local-action'
|
||||||
defaults:
|
defaults:
|
||||||
|
|
@ -102,6 +109,27 @@ menu_test.local_action5:
|
||||||
requirements:
|
requirements:
|
||||||
_access: 'TRUE'
|
_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:
|
menu_test.local_task_test_tasks:
|
||||||
path: '/menu-local-task-test/tasks'
|
path: '/menu-local-task-test/tasks'
|
||||||
defaults:
|
defaults:
|
||||||
|
|
@ -109,6 +137,13 @@ menu_test.local_task_test_tasks:
|
||||||
requirements:
|
requirements:
|
||||||
_access: 'TRUE'
|
_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:
|
menu_test.local_task_test_tasks_tasks:
|
||||||
path: '/menu-local-task-test/tasks/tasks'
|
path: '/menu-local-task-test/tasks/tasks'
|
||||||
defaults:
|
defaults:
|
||||||
|
|
@ -137,6 +172,13 @@ menu_test.local_task_test_tasks_settings:
|
||||||
requirements:
|
requirements:
|
||||||
_access: 'TRUE'
|
_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:
|
menu_test.local_task_test_tasks_settings_sub1:
|
||||||
path: '/menu-local-task-test/tasks/settings/sub1'
|
path: '/menu-local-task-test/tasks/settings/sub1'
|
||||||
defaults:
|
defaults:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Contains \Drupal\menu_test\Plugin\Derivative\LocalActionTest.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\menu_test\Plugin\Derivative;
|
||||||
|
|
||||||
|
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test derivative to check local action title escaping.
|
||||||
|
*
|
||||||
|
* @see \Drupal\system\Tests\Menu\LocalActionTest
|
||||||
|
*/
|
||||||
|
class LocalActionTest extends DeriverBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||||
|
$this->derivatives['example'] = $base_plugin_definition + [
|
||||||
|
'title' => "<script>alert('Welcome to the derived jungle!')</script>",
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->derivatives;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Contains \Drupal\menu_test\Plugin\Derivative\LocalTaskTestWithUnsafeTitle.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\menu_test\Plugin\Derivative;
|
||||||
|
|
||||||
|
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test derivative to check local task title escaping.
|
||||||
|
*
|
||||||
|
* @see \Drupal\system\Tests\Menu\LocalTasksTest
|
||||||
|
*/
|
||||||
|
class LocalTaskTestWithUnsafeTitle extends DeriverBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||||
|
$this->derivatives['unsafe'] = [
|
||||||
|
'title' => "<script>alert('Welcome to the derived jungle!')</script>",
|
||||||
|
'route_parameters' => ['bar' => 'unsafe'],
|
||||||
|
] + $base_plugin_definition;
|
||||||
|
|
||||||
|
return $this->derivatives;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Contains \Drupal\menu_test\Plugin\Menu\ContextualLink\TestContextualLink.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\menu_test\Plugin\Menu\ContextualLink;
|
||||||
|
|
||||||
|
use Drupal\Core\Menu\ContextualLinkDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a contextual link plugin with a dynamic title from user input.
|
||||||
|
*/
|
||||||
|
class TestContextualLink extends ContextualLinkDefault {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getTitle() {
|
||||||
|
return "<script>alert('Welcome to the jungle!')</script>";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -8,12 +8,15 @@
|
||||||
namespace Drupal\menu_test\Plugin\Menu\LocalAction;
|
namespace Drupal\menu_test\Plugin\Menu\LocalAction;
|
||||||
|
|
||||||
use Drupal\Core\Menu\LocalActionDefault;
|
use Drupal\Core\Menu\LocalActionDefault;
|
||||||
|
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines a local action plugin with a dynamic title.
|
* Defines a local action plugin with a dynamic title.
|
||||||
*/
|
*/
|
||||||
class TestLocalAction4 extends LocalActionDefault {
|
class TestLocalAction4 extends LocalActionDefault {
|
||||||
|
|
||||||
|
use StringTranslationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Contains \Drupal\menu_test\Plugin\Menu\LocalAction\TestLocalAction5.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\menu_test\Plugin\Menu\LocalAction;
|
||||||
|
|
||||||
|
use Drupal\Core\Menu\LocalActionDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a local action plugin with a dynamic title from user input.
|
||||||
|
*/
|
||||||
|
class TestLocalAction5 extends LocalActionDefault {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getTitle() {
|
||||||
|
return "<script>alert('Welcome to the jungle!')</script>";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Contains \Drupal\menu_test\Plugin\Menu\LocalTask\TestTaskWithUserInput.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\menu_test\Plugin\Menu\LocalTask;
|
||||||
|
|
||||||
|
use Drupal\Core\Menu\LocalTaskDefault;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
class TestTaskWithUserInput extends LocalTaskDefault {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getTitle(Request $request = NULL) {
|
||||||
|
return "<script>alert('Welcome to the jungle!')</script>";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -8,9 +8,12 @@
|
||||||
namespace Drupal\menu_test\Plugin\Menu\LocalTask;
|
namespace Drupal\menu_test\Plugin\Menu\LocalTask;
|
||||||
|
|
||||||
use Drupal\Core\Menu\LocalTaskDefault;
|
use Drupal\Core\Menu\LocalTaskDefault;
|
||||||
|
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||||
|
|
||||||
class TestTasksSettingsSub1 extends LocalTaskDefault {
|
class TestTasksSettingsSub1 extends LocalTaskDefault {
|
||||||
|
|
||||||
|
use StringTranslationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -61,4 +61,20 @@ class TestControllers {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
namespace Drupal\Tests\Core\Menu;
|
namespace Drupal\Tests\Core\Menu;
|
||||||
|
|
||||||
use Drupal\Core\Menu\ContextualLinkDefault;
|
use Drupal\Core\Menu\ContextualLinkDefault;
|
||||||
|
use Drupal\Core\StringTranslation\TranslationWrapper;
|
||||||
use Drupal\Tests\UnitTestCase;
|
use Drupal\Tests\UnitTestCase;
|
||||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
@ -63,17 +64,18 @@ class ContextualLinkDefaultTest extends UnitTestCase {
|
||||||
|
|
||||||
protected function setupContextualLinkDefault() {
|
protected function setupContextualLinkDefault() {
|
||||||
$this->contextualLinkDefault = new ContextualLinkDefault($this->config, $this->pluginId, $this->pluginDefinition);
|
$this->contextualLinkDefault = new ContextualLinkDefault($this->config, $this->pluginId, $this->pluginDefinition);
|
||||||
$this->contextualLinkDefault->setStringTranslation($this->stringTranslation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers ::getTitle
|
* @covers ::getTitle
|
||||||
*/
|
*/
|
||||||
public function testGetTitle($title = 'Example') {
|
public function testGetTitle() {
|
||||||
$this->pluginDefinition['title'] = $title;
|
$title = 'Example';
|
||||||
|
$this->pluginDefinition['title'] = (new TranslationWrapper($title))
|
||||||
|
->setStringTranslation($this->stringTranslation);
|
||||||
$this->stringTranslation->expects($this->once())
|
$this->stringTranslation->expects($this->once())
|
||||||
->method('translate')
|
->method('translate')
|
||||||
->with($this->pluginDefinition['title'], array(), array())
|
->with($title, array(), array())
|
||||||
->will($this->returnValue('Example translated'));
|
->will($this->returnValue('Example translated'));
|
||||||
|
|
||||||
$this->setupContextualLinkDefault();
|
$this->setupContextualLinkDefault();
|
||||||
|
|
@ -84,11 +86,12 @@ class ContextualLinkDefaultTest extends UnitTestCase {
|
||||||
* @covers ::getTitle
|
* @covers ::getTitle
|
||||||
*/
|
*/
|
||||||
public function testGetTitleWithContext() {
|
public function testGetTitleWithContext() {
|
||||||
$this->pluginDefinition['title'] = 'Example';
|
$title = 'Example';
|
||||||
$this->pluginDefinition['title_context'] = 'context';
|
$this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context')))
|
||||||
|
->setStringTranslation($this->stringTranslation);
|
||||||
$this->stringTranslation->expects($this->once())
|
$this->stringTranslation->expects($this->once())
|
||||||
->method('translate')
|
->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'));
|
->will($this->returnValue('Example translated with context'));
|
||||||
|
|
||||||
$this->setupContextualLinkDefault();
|
$this->setupContextualLinkDefault();
|
||||||
|
|
@ -99,11 +102,12 @@ class ContextualLinkDefaultTest extends UnitTestCase {
|
||||||
* @covers ::getTitle
|
* @covers ::getTitle
|
||||||
*/
|
*/
|
||||||
public function testGetTitleWithTitleArguments() {
|
public function testGetTitleWithTitleArguments() {
|
||||||
$this->pluginDefinition['title'] = 'Example @test';
|
$title = 'Example @test';
|
||||||
$this->pluginDefinition['title_arguments'] = array('@test' => 'value');
|
$this->pluginDefinition['title'] = (new TranslationWrapper($title, array('@test' => 'value')))
|
||||||
|
->setStringTranslation($this->stringTranslation);
|
||||||
$this->stringTranslation->expects($this->once())
|
$this->stringTranslation->expects($this->once())
|
||||||
->method('translate')
|
->method('translate')
|
||||||
->with($this->pluginDefinition['title'], $this->arrayHasKey('@test'), array())
|
->with($title, array('@test' => 'value'), array())
|
||||||
->will($this->returnValue('Example value'));
|
->will($this->returnValue('Example value'));
|
||||||
|
|
||||||
$this->setupContextualLinkDefault();
|
$this->setupContextualLinkDefault();
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
namespace Drupal\Tests\Core\Menu;
|
namespace Drupal\Tests\Core\Menu;
|
||||||
|
|
||||||
use Drupal\Core\Menu\LocalActionDefault;
|
use Drupal\Core\Menu\LocalActionDefault;
|
||||||
|
use Drupal\Core\StringTranslation\TranslationWrapper;
|
||||||
use Drupal\Tests\UnitTestCase;
|
use Drupal\Tests\UnitTestCase;
|
||||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
@ -74,7 +75,6 @@ class LocalActionDefaultTest extends UnitTestCase {
|
||||||
*/
|
*/
|
||||||
protected function setupLocalActionDefault() {
|
protected function setupLocalActionDefault() {
|
||||||
$this->localActionDefault = new LocalActionDefault($this->config, $this->pluginId, $this->pluginDefinition, $this->routeProvider);
|
$this->localActionDefault = new LocalActionDefault($this->config, $this->pluginId, $this->pluginDefinition, $this->routeProvider);
|
||||||
$this->localActionDefault->setStringTranslation($this->stringTranslation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -83,10 +83,11 @@ class LocalActionDefaultTest extends UnitTestCase {
|
||||||
* @see \Drupal\Core\Menu\LocalTaskDefault::getTitle()
|
* @see \Drupal\Core\Menu\LocalTaskDefault::getTitle()
|
||||||
*/
|
*/
|
||||||
public function testGetTitle() {
|
public function testGetTitle() {
|
||||||
$this->pluginDefinition['title'] = 'Example';
|
$this->pluginDefinition['title'] = (new TranslationWrapper('Example'))
|
||||||
|
->setStringTranslation($this->stringTranslation);
|
||||||
$this->stringTranslation->expects($this->once())
|
$this->stringTranslation->expects($this->once())
|
||||||
->method('translate')
|
->method('translate')
|
||||||
->with($this->pluginDefinition['title'], array(), array())
|
->with('Example', array(), array())
|
||||||
->will($this->returnValue('Example translated'));
|
->will($this->returnValue('Example translated'));
|
||||||
|
|
||||||
$this->setupLocalActionDefault();
|
$this->setupLocalActionDefault();
|
||||||
|
|
@ -99,11 +100,11 @@ class LocalActionDefaultTest extends UnitTestCase {
|
||||||
* @see \Drupal\Core\Menu\LocalTaskDefault::getTitle()
|
* @see \Drupal\Core\Menu\LocalTaskDefault::getTitle()
|
||||||
*/
|
*/
|
||||||
public function testGetTitleWithContext() {
|
public function testGetTitleWithContext() {
|
||||||
$this->pluginDefinition['title'] = 'Example';
|
$this->pluginDefinition['title'] = (new TranslationWrapper('Example', array(), array('context' => 'context')))
|
||||||
$this->pluginDefinition['title_context'] = 'context';
|
->setStringTranslation($this->stringTranslation);
|
||||||
$this->stringTranslation->expects($this->once())
|
$this->stringTranslation->expects($this->once())
|
||||||
->method('translate')
|
->method('translate')
|
||||||
->with($this->pluginDefinition['title'], array(), array('context' => 'context'))
|
->with('Example', array(), array('context' => 'context'))
|
||||||
->will($this->returnValue('Example translated with context'));
|
->will($this->returnValue('Example translated with context'));
|
||||||
|
|
||||||
$this->setupLocalActionDefault();
|
$this->setupLocalActionDefault();
|
||||||
|
|
@ -114,11 +115,11 @@ class LocalActionDefaultTest extends UnitTestCase {
|
||||||
* Tests the getTitle method with title arguments.
|
* Tests the getTitle method with title arguments.
|
||||||
*/
|
*/
|
||||||
public function testGetTitleWithTitleArguments() {
|
public function testGetTitleWithTitleArguments() {
|
||||||
$this->pluginDefinition['title'] = 'Example @test';
|
$this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value')))
|
||||||
$this->pluginDefinition['title_arguments'] = array('@test' => 'value');
|
->setStringTranslation($this->stringTranslation);
|
||||||
$this->stringTranslation->expects($this->once())
|
$this->stringTranslation->expects($this->once())
|
||||||
->method('translate')
|
->method('translate')
|
||||||
->with($this->pluginDefinition['title'], $this->arrayHasKey('@test'), array())
|
->with('Example @test', array('@test' => 'value'), array())
|
||||||
->will($this->returnValue('Example value'));
|
->will($this->returnValue('Example value'));
|
||||||
|
|
||||||
$this->setupLocalActionDefault();
|
$this->setupLocalActionDefault();
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ namespace Drupal\Tests\Core\Menu;
|
||||||
use Drupal\Core\Menu\LocalTaskDefault;
|
use Drupal\Core\Menu\LocalTaskDefault;
|
||||||
use Drupal\Core\Routing\RouteMatch;
|
use Drupal\Core\Routing\RouteMatch;
|
||||||
use Drupal\Core\Routing\RouteProviderInterface;
|
use Drupal\Core\Routing\RouteProviderInterface;
|
||||||
|
use Drupal\Core\StringTranslation\TranslationWrapper;
|
||||||
use Drupal\Tests\UnitTestCase;
|
use Drupal\Tests\UnitTestCase;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\Routing\Route;
|
use Symfony\Component\Routing\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -77,9 +77,7 @@ class LocalTaskDefaultTest extends UnitTestCase {
|
||||||
protected function setupLocalTaskDefault() {
|
protected function setupLocalTaskDefault() {
|
||||||
$this->localTaskBase = new TestLocalTaskDefault($this->config, $this->pluginId, $this->pluginDefinition);
|
$this->localTaskBase = new TestLocalTaskDefault($this->config, $this->pluginId, $this->pluginDefinition);
|
||||||
$this->localTaskBase
|
$this->localTaskBase
|
||||||
->setRouteProvider($this->routeProvider)
|
->setRouteProvider($this->routeProvider);
|
||||||
->setStringTranslation($this->stringTranslation);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -234,11 +232,12 @@ class LocalTaskDefaultTest extends UnitTestCase {
|
||||||
/**
|
/**
|
||||||
* @covers ::getTitle
|
* @covers ::getTitle
|
||||||
*/
|
*/
|
||||||
public function testGetTitleWithoutContext() {
|
public function testGetTitle() {
|
||||||
$this->pluginDefinition['title'] = 'Example';
|
$this->pluginDefinition['title'] = (new TranslationWrapper('Example'))
|
||||||
|
->setStringTranslation($this->stringTranslation);
|
||||||
$this->stringTranslation->expects($this->once())
|
$this->stringTranslation->expects($this->once())
|
||||||
->method('translate')
|
->method('translate')
|
||||||
->with($this->pluginDefinition['title'], array(), array())
|
->with('Example', array(), array())
|
||||||
->will($this->returnValue('Example translated'));
|
->will($this->returnValue('Example translated'));
|
||||||
|
|
||||||
$this->setupLocalTaskDefault();
|
$this->setupLocalTaskDefault();
|
||||||
|
|
@ -249,11 +248,12 @@ class LocalTaskDefaultTest extends UnitTestCase {
|
||||||
* @covers ::getTitle
|
* @covers ::getTitle
|
||||||
*/
|
*/
|
||||||
public function testGetTitleWithContext() {
|
public function testGetTitleWithContext() {
|
||||||
$this->pluginDefinition['title'] = 'Example';
|
$title = 'Example';
|
||||||
$this->pluginDefinition['title_context'] = 'context';
|
$this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context')))
|
||||||
|
->setStringTranslation($this->stringTranslation);
|
||||||
$this->stringTranslation->expects($this->once())
|
$this->stringTranslation->expects($this->once())
|
||||||
->method('translate')
|
->method('translate')
|
||||||
->with($this->pluginDefinition['title'], array(), array('context' => 'context'))
|
->with($title, array(), array('context' => 'context'))
|
||||||
->will($this->returnValue('Example translated with context'));
|
->will($this->returnValue('Example translated with context'));
|
||||||
|
|
||||||
$this->setupLocalTaskDefault();
|
$this->setupLocalTaskDefault();
|
||||||
|
|
@ -264,16 +264,16 @@ class LocalTaskDefaultTest extends UnitTestCase {
|
||||||
* @covers ::getTitle
|
* @covers ::getTitle
|
||||||
*/
|
*/
|
||||||
public function testGetTitleWithTitleArguments() {
|
public function testGetTitleWithTitleArguments() {
|
||||||
$this->pluginDefinition['title'] = 'Example @test';
|
$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())
|
$this->stringTranslation->expects($this->once())
|
||||||
->method('translate')
|
->method('translate')
|
||||||
->with($this->pluginDefinition['title'], $this->arrayHasKey('@test'), array())
|
->with($title, array('@test' => 'value'), array())
|
||||||
->will($this->returnValue('Example value'));
|
->will($this->returnValue('Example value'));
|
||||||
|
|
||||||
$this->setupLocalTaskDefault();
|
$this->setupLocalTaskDefault();
|
||||||
$request = new Request();
|
$this->assertEquals('Example value', $this->localTaskBase->getTitle());
|
||||||
$this->assertEquals('Example value', $this->localTaskBase->getTitle($request));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@
|
||||||
|
|
||||||
namespace Drupal\Tests\Core\Plugin\Discovery;
|
namespace Drupal\Tests\Core\Plugin\Discovery;
|
||||||
|
|
||||||
|
use Drupal\Core\StringTranslation\TranslationWrapper;
|
||||||
use Drupal\Tests\UnitTestCase;
|
use Drupal\Tests\UnitTestCase;
|
||||||
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
|
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
|
||||||
|
use org\bovigo\vfs\vfsStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @coversDefaultClass \Drupal\Core\Plugin\Discovery\YamlDiscovery
|
* @coversDefaultClass \Drupal\Core\Plugin\Discovery\YamlDiscovery
|
||||||
|
|
@ -70,6 +72,44 @@ class YamlDiscoveryTest extends UnitTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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.
|
* Tests the getDefinition() method.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue