Commit 1a8858a0 authored by alexpott's avatar alexpott

Issue #2144413 by tstoeckler, YesCT, robertdbailey, webflo, Schnitzel, Wim...

Issue #2144413 by tstoeckler, YesCT, robertdbailey, webflo, Schnitzel, Wim Leers, kfritsche, Jose Reyero, Gábor Hojtsy, prodosh: Config translation does not support text elements with a format
parent 9f6975a8
......@@ -737,3 +737,26 @@ field.float.value:
value:
type: float
label: 'Value'
# Text with a text format.
text_format:
type: mapping
label: 'Text with text format'
# We declare the entire mapping of text and text format as translatable. This
# causes the entire mapping to be saved to the language overrides of the
# configuration. Storing only the (to be formatted) text could result in
# security problems in case the text format of the source text is changed.
translatable: true
mapping:
value:
type: text
label: 'Text'
# Mark the actual text as translatable (in addition to the entire mapping
# being marked as translatable) so that shipped configuration with
# formatted text can participate in the string translation system.
translatable: true
format:
type: string
label: 'Text format'
# The text format should not be translated as part of the string
# translation system, so this is not marked as translatable.
......@@ -9,6 +9,8 @@
/**
* Defines events for the configuration system.
*
* @see \Drupal\Core\Config\ConfigCrudEvent
*/
final class ConfigEvents {
......
......@@ -24,7 +24,7 @@
* @param string $name
* Configuration object name.
*
* @return \Drupal\Core\Config\Schema\Element
* @return \Drupal\Core\TypedData\TraversableTypedDataInterface
* Typed configuration element.
*/
public function get($name);
......
......@@ -173,10 +173,22 @@ function config_translation_entity_operation(EntityInterface $entity) {
* Implements hook_config_schema_info_alter().
*/
function config_translation_config_schema_info_alter(&$definitions) {
$map = array(
'label' => '\Drupal\config_translation\FormElement\Textfield',
'text' => '\Drupal\config_translation\FormElement\Textarea',
'date_format' => '\Drupal\config_translation\FormElement\DateFormat',
'text_format' => '\Drupal\config_translation\FormElement\TextFormat',
'mapping' => '\Drupal\config_translation\FormElement\ListElement',
'sequence' => '\Drupal\config_translation\FormElement\ListElement',
);
// Enhance the text and date type definitions with classes to generate proper
// form elements in ConfigTranslationFormBase. Other translatable types will
// appear as a one line textfield.
$definitions['text']['form_element_class'] = '\Drupal\config_translation\FormElement\Textarea';
$definitions['date_format']['form_element_class'] = '\Drupal\config_translation\FormElement\DateFormat';
foreach ($definitions as $type => &$definition) {
if (isset($map[$type]) && !isset($definition['form_element_class'])) {
$definition['form_element_class'] = $map[$type];
}
}
}
......@@ -12,34 +12,29 @@
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\TypedData\DataDefinitionInterface;
/**
* Defines the date format element for the configuration translation interface.
*/
class DateFormat implements ElementInterface {
use StringTranslationTrait;
class DateFormat extends FormElementBase {
/**
* {@inheritdoc}
*/
public function getFormElement(DataDefinitionInterface $definition, LanguageInterface $language, $value) {
public function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) {
$description = $this->t('A user-defined date format. See the <a href="@url">PHP manual</a> for available options.', array('@url' => 'http://php.net/manual/function.date.php'));
$format = $this->t('Displayed as %date_format', array('%date_format' => \Drupal::service('date.formatter')->format(REQUEST_TIME, 'custom', $value)));
$format = $this->t('Displayed as %date_format', array('%date_format' => \Drupal::service('date.formatter')->format(REQUEST_TIME, 'custom', $translation_config)));
return array(
'#type' => 'textfield',
'#title' => $this->t($definition->getLabel()) . '<span class="visually-hidden"> (' . $language->getName() . ')</span>',
'#description' => $description,
'#default_value' => $value,
'#attributes' => array('lang' => $language->getId()),
'#field_suffix' => ' <div class="edit-date-format-suffix"><small id="edit-date-format-suffix">' . $format . '</small></div>',
'#ajax' => array(
'callback' => 'Drupal\config_translation\FormElement\DateFormat::ajaxSample',
'event' => 'keyup',
'progress' => array('type' => 'throbber', 'message' => NULL),
),
);
) + parent::getTranslationElement($translation_language, $source_config, $translation_config);
}
/**
......
......@@ -7,8 +7,10 @@
namespace Drupal\config_translation\FormElement;
use Drupal\Core\Config\Config;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\language\Config\LanguageConfigOverride;
/**
* Provides an interface for configuration translation form elements.
......@@ -16,19 +18,56 @@
interface ElementInterface {
/**
* Returns the translation form element for a given configuration definition.
* Creates a form element instance from a schema definition.
*
* @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
* Configuration schema type definition of the element.
* @param \Drupal\Core\Language\LanguageInterface $language
* Language object to display the translation form for.
* @param string $value
* Default value for the form element.
* @param \Drupal\Core\TypedData\TypedDataInterface $schema
* The configuration schema.
*
* @return static
*/
public static function create(TypedDataInterface $schema);
/**
* Builds a render array containg the source and translation form elements.
*
* @param \Drupal\Core\Language\LanguageInterface $source_language
* The source language of the configuration object.
* @param \Drupal\Core\Language\LanguageInterface $translation_language
* The language to display the translation form for.
* @param mixed $source_config
* The configuration value of the element in the source language.
* @param mixed $translation_config
* The configuration value of the element in the language to translate to.
* @param array $parents
* Parents array for the element in the form.
* @param string|null $base_key
* (optional) Base key to be used for the elements in the form. NULL for
* top-level form elements.
*
* @return array
* Form API array to represent the form element.
* A render array consisting of the source and translation elements for the
* source value.
*/
public function getFormElement(DataDefinitionInterface $definition, LanguageInterface $language, $value);
public function getTranslationBuild(LanguageInterface $source_language, LanguageInterface $translation_language, $source_config, $translation_config, array $parents, $base_key = NULL);
/**
* Sets configuration based on a nested form value array.
*
* If the configuration values are the same as the source configuration, the
* override should be removed from the translation configuration.
*
* @param \Drupal\Core\Config\Config $base_config
* Base configuration values, in the source language.
* @param \Drupal\language\Config\LanguageConfigOverride $config_translation
* Translation configuration override data.
* @param mixed $config_values
* The configuration value of the element taken from the form values.
* @param string|null $base_key
* (optional) The base key that the schema and the configuration values
* belong to. This should be NULL for the top-level configuration object and
* be populated consecutively when recursing into the configuration
* structure.
*/
public function setConfig(Config $base_config, LanguageConfigOverride $config_translation, $config_values, $base_key = NULL);
}
<?php
/**
* @file
* Contains \Drupal\config_translation\FormElement\FormElementBase.
*/
namespace Drupal\config_translation\FormElement;
use Drupal\Core\Config\Config;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\language\Config\LanguageConfigOverride;
/**
* Provides a common base class for form elements.
*/
abstract class FormElementBase implements ElementInterface {
use StringTranslationTrait;
/**
* The schema element this form is for.
*
* @var \Drupal\Core\TypedData\TypedDataInterface
*/
protected $element;
/**
* The data definition of the element this form element is for.
*
* @var \Drupal\Core\TypedData\DataDefinitionInterface
*/
protected $definition;
/**
* Constructs a FormElementBase.
*
* @param \Drupal\Core\TypedData\TypedDataInterface $element
* The schema element this form element is for.
*/
public function __construct(TypedDataInterface $element) {
$this->element = $element;
$this->definition = $element->getDataDefinition();
}
/**
* {@inheritdoc}
*/
public static function create(TypedDataInterface $schema) {
return new static($schema);
}
/**
* {@inheritdoc}
*/
public function getTranslationBuild(LanguageInterface $source_language, LanguageInterface $translation_language, $source_config, $translation_config, array $parents, $base_key = NULL) {
$build['#theme'] = 'config_translation_manage_form_element';
// For accessibility we make source and translation appear next to each
// other in the source for each element, which is why we utilize the
// 'source' and 'translation' sub-keys for the form. The form values,
// however, should mirror the configuration structure, so that we can
// traverse the configuration schema and still access the right
// configuration values in ConfigTranslationFormBase::setConfig().
// Therefore we make the 'source' and 'translation' keys the top-level
// keys in $form_state['values'].
$build['source'] = $this->getSourceElement($source_language, $source_config);
$build['translation'] = $this->getTranslationElement($translation_language, $source_config, $translation_config);
$build['source']['#parents'] = array_merge(array('source'), $parents);
$build['translation']['#parents'] = array_merge(array('translation'), $parents);
return $build;
}
/**
* Returns the source element for a given configuration definition.
*
* This can be either a render array that actually outputs the source values
* directly or a read-only form element with the source values depending on
* what is considered to provide a more intuitive user interface for the
* translator.
*
* @param \Drupal\Core\Language\LanguageInterface $source_language
* Thee source language of the configuration object.
* @param mixed $source_config
* The configuration value of the element in the source language.
*
* @return array
* A render array for the source value.
*/
protected function getSourceElement(LanguageInterface $source_language, $source_config) {
if ($source_config) {
$value = '<span lang="' . $source_language->getId() . '">' . nl2br($source_config) . '</span>';
}
else {
$value = $this->t('(Empty)');
}
return array(
'#type' => 'item',
'#title' => $this->t('!label <span class="visually-hidden">(!source_language)</span>', array(
'!label' => $this->t($this->definition->getLabel()),
'!source_language' => $source_language->getName(),
)),
'#markup' => $value,
);
}
/**
* Returns the translation form element for a given configuration definition.
*
* For complex data structures (such as mappings) that are translatable
* wholesale but contain non-translatable properties, the form element is
* responsible for checking access to the source value of those properties. In
* case of formatted text, for example, access to the source text format must
* be checked. If the translator does not have access to the text format, the
* textarea must be disabled and the translator may not be able to translate
* this particular configuration element. If the translator does have access
* to the text format, the element must be locked down to that particular text
* format; in other words, the format may not be changed by the translator
* (because the text format property is not itself translatable).
*
* In addition, the form element is responsible for checking whether the
* value of such non-translatable properties in the translated configuration
* is equal to the corresponding source values. If not, that means that the
* source value has changed after the translation was added. In this case -
* again - the translation of this element must be disabled if the translator
* does not have access to the source value of the non-translatable property.
* For example, if a formatted text element, whose source format was plain
* text when it was first translated, gets changed to the Full HTML format,
* simply changing the format of the translation would lead to an XSS
* vulnerability as the translated text, that was intended to be escaped,
* would now be displayed unescaped. Thus, if the translator does not have
* access to the Full HTML format, the translation for this particular element
* may not be updated at all (the textarea must be disabled). Only if access
* to the Full HTML format is granted, an explicit translation taking into
* account the updated source value(s) may be submitted.
*
* In the specific case of formatted text this logic is implemented by
* utilizing a form element of type 'text_format' and its #format and
* #allowed_formats properties. The access logic explained above is then
* handled by the 'text_format' element itself, specifically by
* filter_process_format(). In case such a rich element is not available for
* translation of complex data, similar access logic must be implemented
* manually.
*
* @param \Drupal\Core\Language\LanguageInterface $translation_language
* The language to display the translation form for.
* @param mixed $source_config
* The configuration value of the element in the source language.
* @param mixed $translation_config
* The configuration value of the element in the language to translate to.
*
* @return array
* Form API array to represent the form element.
*
* @see \Drupal\config_translation\FormElement\TextFormat
* @see filter_process_format()
*/
protected function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) {
// Add basic properties that apply to all form elements.
return array(
'#title' => $this->t('!label <span class="visually-hidden">(!source_language)</span>', array(
'!label' => $this->t($this->definition['label']),
'!source_language' => $translation_language->getName(),
)),
'#default_value' => $translation_config,
'#attributes' => array('lang' => $translation_language->getId()),
);
}
/**
* {@inheritdoc}
*/
public function setConfig(Config $base_config, LanguageConfigOverride $config_translation, $config_values, $base_key = NULL) {
// Save the configuration values, if they are different from the source
// values in the base configuration. Otherwise remove the override.
if ($base_config->get($base_key) !== $config_values) {
$config_translation->set($base_key, $config_values);
}
else {
$config_translation->clear($base_key);
}
}
}
<?php
/**
* @file
* Contains \Drupal\config_translation\FormElement\ListElement.
*/
namespace Drupal\config_translation\FormElement;
use Drupal\Core\Config\Config;
use Drupal\Core\Language\LanguageInterface;
use Drupal\config_translation\Form\ConfigTranslationFormBase;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Core\TypedData\TraversableTypedDataInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\language\Config\LanguageConfigOverride;
/**
* Defines the list element for the configuration translation interface.
*/
class ListElement implements ElementInterface {
use StringTranslationTrait;
/**
* The schema element this form is for.
*
* @var \Drupal\Core\TypedData\TraversableTypedDataInterface
*/
protected $element;
/**
* Constructs a ListElement.
*
* @param \Drupal\Core\TypedData\TraversableTypedDataInterface $element
* The schema element this form element is for.
*/
public function __construct(TraversableTypedDataInterface $element) {
$this->element = $element;
}
/**
* {@inheritdoc}
*/
public static function create(TypedDataInterface $schema) {
return new static($schema);
}
/**
* {@inheritdoc}
*/
public function getTranslationBuild(LanguageInterface $source_language, LanguageInterface $translation_language, $source_config, $translation_config, array $parents, $base_key = NULL) {
$build = array();
foreach ($this->element as $key => $element) {
$sub_build = array();
// Make the specific element key, "$base_key.$key".
$element_key = implode('.', array_filter(array($base_key, $key)));
$definition = $element->getDataDefinition();
if ($form_element = ConfigTranslationFormBase::createFormElement($element)) {
$element_parents = array_merge($parents, array($key));
$sub_build += $form_element->getTranslationBuild($source_language, $translation_language, $source_config[$key], $translation_config[$key], $element_parents, $element_key);
if (empty($sub_build)) {
continue;
}
// Build the sub-structure and include it with a wrapper in the form if
// there are any translatable elements there.
$build[$key] = array();
if ($element instanceof TraversableTypedDataInterface) {
$build[$key] = array(
'#type' => 'details',
'#title' => $this->getGroupTitle($definition, $sub_build),
'#open' => empty($base_key),
);
}
$build[$key] += $sub_build;
}
}
return $build;
}
/**
* {@inheritdoc}
*/
public function setConfig(Config $base_config, LanguageConfigOverride $config_translation, $config_values, $base_key = NULL) {
foreach ($this->element as $key => $element) {
$element_key = implode('.', array_filter(array($base_key, $key)));
if ($form_element = ConfigTranslationFormBase::createFormElement($element)) {
// Traverse into the next level of the configuration.
$value = isset($config_values[$key]) ? $config_values[$key] : NULL;
$form_element->setConfig($base_config, $config_translation, $value, $element_key);
}
}
}
/**
* Returns the title for the 'details' element of a group of schema elements.
*
* For some configuration elements the same element structure can be repeated
* multiple times (for example views displays, filters, etc.). Thus, we try to
* find a more usable title for the details summary. First check if there is
* an element which is called title or label and use its value. Then check if
* there is an element which contains these words and use those. Fall back
* to the generic definition label if no such element is found.
*
* @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
* The definition of the schema element.
* @param array $group_build
* The renderable array for the group of schema elements.
*
* @return string
* The title for the group of schema elements.
*/
protected function getGroupTitle(DataDefinitionInterface $definition, array $group_build) {
$title = '';
if (isset($group_build['title']['source'])) {
$title = $group_build['title']['source']['#markup'];
}
elseif (isset($group_build['label']['source'])) {
$title = $group_build['label']['source']['#markup'];
}
else {
foreach (array_keys($group_build) as $title_key) {
if (isset($group_build[$title_key]['source']) && (strpos($title_key, 'title') !== FALSE || strpos($title_key, 'label') !== FALSE)) {
$title = $group_build[$title_key]['source']['#markup'];
break;
}
}
}
return (!empty($title) ? (strip_tags($title) . ' ') : '') . $this->t($definition['label']);
}
}
<?php
/**
* @file
* Contains \Drupal\config_translation\FormElement\TextFormat.
*/
namespace Drupal\config_translation\FormElement;
use Drupal\Core\Language\LanguageInterface;
/**
* Defines the text_format element for the configuration translation interface.
*/
class TextFormat extends FormElementBase {
/**
* {@inheritdoc}
*/
public function getSourceElement(LanguageInterface $source_language, $source_config) {
// Instead of the formatted output show a disabled textarea. This allows for
// easier side-by-side comparison, especially with formats with text
// editors.
return $this->getTranslationElement($source_language, $source_config, $source_config) + array(
'#value' => $source_config['value'],
'#disabled' => TRUE,
'#allow_focus' => TRUE,
);
}
/**
* {@inheritdoc}
*/
public function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) {
return array(
'#type' => 'text_format',
// Override the #default_value property from the parent class.
'#default_value' => $translation_config['value'],
'#format' => $translation_config['format'],
// @see \Drupal\config_translation\Element\FormElementBase::getTranslationElement()
'#allowed_formats' => array($source_config['format']),
) + parent::getTranslationElement($translation_language, $source_config, $translation_config);
}
}
......@@ -8,31 +8,25 @@
namespace Drupal\config_translation\FormElement;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\TypedData\DataDefinitionInterface;
/**
* Defines the textarea element for the configuration translation interface.
*/
class Textarea implements ElementInterface {
use StringTranslationTrait;
class Textarea extends FormElementBase {
/**
* {@inheritdoc}
*/
public function getFormElement(DataDefinitionInterface $definition, LanguageInterface $language, $value) {
public function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) {
// Estimate a comfortable size of the input textarea.
$rows_words = ceil(str_word_count($value) / 5);
$rows_newlines = substr_count($value, "\n" ) + 1;
$rows_words = ceil(str_word_count($translation_config) / 5);
$rows_newlines = substr_count($translation_config, "\n" ) + 1;
$rows = max($rows_words, $rows_newlines);
return array(
'#type' => 'textarea',
'#default_value' => $value,
'#title' => $this->t($definition->getLabel()) . '<span class="visually-hidden"> (' . $language->getName() . ')</span>',
'#rows' => $rows,
'#attributes' => array('lang' => $language->getId()),
);
) + parent::getTranslationElement($translation_language, $source_config, $translation_config);
}
}
......@@ -8,25 +8,19 @@
namespace Drupal\config_translation\FormElement;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\TypedData\DataDefinitionInterface;
/**
* Defines the textfield element for the configuration translation interface.
*/
class Textfield implements ElementInterface {
use StringTranslationTrait;
class Textfield extends FormElementBase {
/**
* {@inheritdoc}
*/
public function getFormElement(DataDefinitionInterface $definition, LanguageInterface $language, $value) {
public function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) {
return array(
'#type' => 'textfield',
'#default_value' => $value,
'#title' => $this->t($definition->getLabel()) . '<span class="visually-hidden"> (' . $language->getName() . ')</span>',
'#attributes' => array('lang' => $language->getId()),
);
) + parent::getTranslationElement($translation_language, $source_config, $translation_config);
}
}
......@@ -22,7 +22,7 @@ class ConfigTranslationFormTest extends WebTestBase {
*
* @var array
*/
public static $modules = array('config_translation', 'config_translation_test');
public static $modules = array('config_translation', 'config_translation_test', 'editor');
/**
* The plugin ID of the mapper to test.
......
id: test
label: 'Test'
langcode: en
content:
value: "<p><strong>Hello World</strong></p>"
format: plain_text
# Schema for the configuration files of the Configuration translation test module.
config_translation_test.content:
type: mapping
label: 'Content'
mapping:
id:
type: string
label: 'Category identifier'
label:
type: label
label: 'Label'
langcode:
type: string
label: 'Default language'
content:
type: text_format
label: 'Content'
# Attach to file settings for testing. The base route does not matter.
system.file_system_settings:
title: 'Test config translation'
base_route_name: system.file_system_settings
names:
- config_translation_test.content
......@@ -5,4 +5,5 @@ package: Testing
version: VERSION
core: 8.x
dependencies:
- config_translation
- config_test
</
# Add a default local task for the file system settings page, so that the local