diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index b906f72..8cc01af 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -329,7 +329,7 @@ text_format:
type: text
label: 'Text'
# locale.module integrates the language overrides of shipped configuration
- # with http://localize.drupal.org. Because it only handles strings and
+ # with https://localize.drupal.org. Because it only handles strings and
# cannot deal with complex data structures, it parses the configuration
# schema until it reaches a primitive and only then checks whether the
# element is translatable.
@@ -340,4 +340,4 @@ text_format:
# Even though the entire 'text_format' is marked as translatable for the
# sake of language configuration overrides, the ID of the text format of
# texts in shipped configuration should not be exposed to
- # http://localize.drupal.org
+ # https://localize.drupal.org
diff --git a/core/lib/Drupal/Core/Config/ConfigEvents.php b/core/lib/Drupal/Core/Config/ConfigEvents.php
index a4b6446..2b2d2c8 100644
--- a/core/lib/Drupal/Core/Config/ConfigEvents.php
+++ b/core/lib/Drupal/Core/Config/ConfigEvents.php
@@ -10,7 +10,7 @@
/**
* Defines events for the configuration system.
*
- * @see \Drupal\Core\Config\ConfiguCrudEvent
+ * @see \Drupal\Core\Config\ConfigCrudEvent
*/
final class ConfigEvents {
diff --git a/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php
index a0e20d5..ede2965 100644
--- a/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php
+++ b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php
@@ -289,7 +289,7 @@ protected function buildConfigForm($name, ArrayElement $schema, $config_data, $b
'#theme' => 'config_translation_manage_form_element',
);
$build[$element_key]['source'] = $form_element->getSourceElement($definition, $this->sourceLanguage, $base_config_data[$key]);
- $build[$element_key]['translation'] = $form_element->getTranslationElement($definition, $this->language, $config_data[$key]);
+ $build[$element_key]['translation'] = $form_element->getTranslationElement($definition, $this->language, $base_config_data[$key], $config_data[$key]);
// 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,
diff --git a/core/modules/config_translation/src/FormElement/DateFormat.php b/core/modules/config_translation/src/FormElement/DateFormat.php
index 5f0a29b..cd3e51a 100644
--- a/core/modules/config_translation/src/FormElement/DateFormat.php
+++ b/core/modules/config_translation/src/FormElement/DateFormat.php
@@ -20,14 +20,9 @@ class DateFormat extends FormElementBase {
/**
* {@inheritdoc}
*/
- public function getTranslationElement(array $definition, LanguageInterface $language, $value) {
- if (class_exists('intlDateFormatter')) {
- $description = $this->t('A user-defined date format. See the PHP manual for available options.', array('@url' => 'http://userguide.icu-project.org/formatparse/datetime'));
- }
- else {
- $description = $this->t('A user-defined date format. See the PHP manual 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')->format(REQUEST_TIME, 'custom', $value)));
+ public function getTranslationElement(array $definition, LanguageInterface $translation_language, $source_config, $translation_config) {
+ $description = $this->t('A user-defined date format. See the PHP manual 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')->format(REQUEST_TIME, 'custom', $translation_config)));
return array(
'#type' => 'textfield',
@@ -38,7 +33,7 @@ public function getTranslationElement(array $definition, LanguageInterface $lang
'event' => 'keyup',
'progress' => array('type' => 'throbber', 'message' => NULL),
),
- ) + parent::getTranslationElement($definition, $language, $value);
+ ) + parent::getTranslationElement($definition, $translation_language, $source_config, $translation_config);
}
/**
diff --git a/core/modules/config_translation/src/FormElement/ElementInterface.php b/core/modules/config_translation/src/FormElement/ElementInterface.php
index 07ddf47..c3b66dd 100644
--- a/core/modules/config_translation/src/FormElement/ElementInterface.php
+++ b/core/modules/config_translation/src/FormElement/ElementInterface.php
@@ -15,33 +15,78 @@
interface ElementInterface {
/**
- * Returns the translation form element for a given configuration definition.
+ * Returns the source element for a given configuration definition.
+ *
+ * This can be either a renderable 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 array $definition
* The configuration schema for the element.
- * @param \Drupal\Core\Language\LanguageInterface $language
- * The language to display the translation form for.
- * @param string $value
- * Default value for the form element.
+ * @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
- * Form API array to represent the form element.
+ * A render array for the source value.
*/
- public function getTranslationElement(array $definition, LanguageInterface $language, $value);
+ public function getSourceElement(array $definition, LanguageInterface $source_language, $source_config);
/**
- * Returns the source element for a given configuration definition.
+ * 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 array $definition
* The configuration schema for the element.
- * @param \Drupal\Core\Language\LanguageInterface $source_language
- * Thee source language of the configuration object.
- * @param array|string $base_config
+ * @param \Drupal\Core\Language\LanguageInterface $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
- * A render array for the source value.
+ * Form API array to represent the form element.
+ *
+ * @see \Drupal\config_translation\FormElement\TextFormat
+ * @see filter_process_format()
*/
- public function getSourceElement(array $definition, LanguageInterface $source_language, $base_config);
+ public function getTranslationElement(array $definition, LanguageInterface $language, $source_config, $translation_config);
}
diff --git a/core/modules/config_translation/src/FormElement/FormElementBase.php b/core/modules/config_translation/src/FormElement/FormElementBase.php
index 6199024..6446808 100644
--- a/core/modules/config_translation/src/FormElement/FormElementBase.php
+++ b/core/modules/config_translation/src/FormElement/FormElementBase.php
@@ -20,26 +20,9 @@
/**
* {@inheritdoc}
*/
- public function getTranslationElement(array $definition, LanguageInterface $language, $value) {
- // Add basic properties that apply to all form elements.
- return array(
- '#title' => $this->t(
- '!label (!source_language)',
- array(
- '!label' => $this->t($definition['label']),
- '!source_language' => $language->getName(),
- )
- ), '#default_value' => $value,
- '#attributes' => array('lang' => $language->getId()),
- );
- }
-
- /**
- * {@inheritdoc}
- */
- public function getSourceElement(array $definition, LanguageInterface $source_language, $base_config) {
- if ($base_config) {
- $value = '' . nl2br($base_config) . '';
+ public function getSourceElement(array $definition, LanguageInterface $source_language, $source_config) {
+ if ($source_config) {
+ $value = '' . nl2br($source_config) . '';
}
else {
$value = $this->t('(Empty)');
@@ -47,15 +30,28 @@ public function getSourceElement(array $definition, LanguageInterface $source_la
return array(
'#type' => 'item',
- '#title' => $this->t(
- '!label (!source_language)',
- array(
- '!label' => $this->t($definition['label']),
- '!source_language' => $source_language->getName(),
- )
- ),
+ '#title' => $this->t('!label (!source_language)', array(
+ '!label' => $this->t($definition['label']),
+ '!source_language' => $source_language->getName(),
+ )),
'#markup' => $value,
);
}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTranslationElement(array $definition, LanguageInterface $translation_language, $source_config, $translation_config) {
+ // Add basic properties that apply to all form elements.
+ return array(
+ '#title' => $this->t('!label (!source_language)', array(
+ '!label' => $this->t($definition['label']),
+ '!source_language' => $translation_language->getName(),
+ )),
+ '#default_value' => $translation_config,
+ '#attributes' => array('lang' => $translation_language->getId()),
+ );
+ }
+
}
diff --git a/core/modules/config_translation/src/FormElement/TextFormat.php b/core/modules/config_translation/src/FormElement/TextFormat.php
index 451c204..7d973be 100644
--- a/core/modules/config_translation/src/FormElement/TextFormat.php
+++ b/core/modules/config_translation/src/FormElement/TextFormat.php
@@ -8,7 +8,6 @@
namespace Drupal\config_translation\FormElement;
use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Defines the text_format element for the configuration translation interface.
@@ -18,29 +17,29 @@ class TextFormat extends FormElementBase {
/**
* {@inheritdoc}
*/
- public function getTranslationElement(array $definition, LanguageInterface $language, $value) {
- // Override the #default_value property from the parent class.
- return array(
- '#type' => 'text_format',
- '#default_value' => $value['value'],
- '#format' => $value['format'],
- '#allowed_formats' => array($value['format']),
- ) + parent::getTranslationElement($definition, $language, $value);
- }
-
- /**
- * {@inheritdoc}
- */
- public function getSourceElement(array $definition, LanguageInterface $source_language, $base_config) {
+ public function getSourceElement(array $definition, 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.
- $element = $this->getTranslationElement($definition, $source_language, $base_config) + array(
- '#value' => $base_config['value'],
+ return $this->getTranslationElement($definition, $source_language, $source_config, $source_config) + array(
+ '#value' => $source_config['value'],
'#disabled' => TRUE,
'#allow_focus' => TRUE,
);
- return $element;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getTranslationElement(array $definition, 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'],
+ '#allowed_formats' => array($source_config['format']),
+ ) + parent::getTranslationElement($definition, $translation_language, $source_config, $translation_config);
+ }
+
+
}
diff --git a/core/modules/config_translation/src/FormElement/Textarea.php b/core/modules/config_translation/src/FormElement/Textarea.php
index d276249..7479a54 100644
--- a/core/modules/config_translation/src/FormElement/Textarea.php
+++ b/core/modules/config_translation/src/FormElement/Textarea.php
@@ -17,16 +17,16 @@ class Textarea extends FormElementBase {
/**
* {@inheritdoc}
*/
- public function getTranslationElement(array $definition, LanguageInterface $language, $value) {
+ public function getTranslationElement(array $definition, 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',
'#rows' => $rows,
- ) + parent::getTranslationElement($definition, $language, $value);
+ ) + parent::getTranslationElement($definition, $translation_language, $source_config, $translation_config);
}
}
diff --git a/core/modules/config_translation/src/FormElement/Textfield.php b/core/modules/config_translation/src/FormElement/Textfield.php
index 687fdfb..eff5f7c 100644
--- a/core/modules/config_translation/src/FormElement/Textfield.php
+++ b/core/modules/config_translation/src/FormElement/Textfield.php
@@ -17,10 +17,10 @@ class Textfield extends FormElementBase {
/**
* {@inheritdoc}
*/
- public function getTranslationElement(array $definition, LanguageInterface $language, $value) {
+ public function getTranslationElement(array $definition, LanguageInterface $translation_language, $source_config, $translation_config) {
return array(
'#type' => 'textfield',
- ) + parent::getTranslationElement($definition, $language, $value);
+ ) + parent::getTranslationElement($definition, $translation_language, $source_config, $translation_config);
}
}
diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php
index cb9f27a..41dd224 100644
--- a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php
+++ b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php
@@ -8,6 +8,7 @@
namespace Drupal\config_translation\Tests;
use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Language\Language;
@@ -24,7 +25,7 @@ class ConfigTranslationUiTest extends WebTestBase {
*
* @var array
*/
- public static $modules = array('node', 'contact', 'config_translation', 'config_translation_test', 'views', 'views_ui', 'contextual', 'filter');
+ public static $modules = array('node', 'contact', 'config_translation', 'config_translation_test', 'views', 'views_ui', 'contextual', 'filter', 'filter_test');
/**
* Languages to enable.
@@ -67,12 +68,24 @@ public function setUp() {
$translator_permissions = array(
'translate configuration',
);
+
+ /** @var \Drupal\filter\FilterFormatInterface $filter_test_format */
+ $filter_test_format = entity_load('filter_format', 'filter_test');
+ /** @var \Drupal\filter\FilterFormatInterface $filtered_html_format */
+ $filtered_html_format = entity_load('filter_format', 'filtered_html');
+ /** @var \Drupal\filter\FilterFormatInterface $full_html_format */
+ $full_html_format = entity_load('filter_format', 'full_html');
+
$admin_permissions = array_merge(
$translator_permissions,
array(
'administer languages',
'administer site configuration',
'administer contact forms',
+ 'administer filters',
+ $filtered_html_format->getPermissionName(),
+ $full_html_format->getPermissionName(),
+ $filter_test_format->getPermissionName(),
'access site-wide contact form',
'access contextual links',
'administer views',
@@ -659,8 +672,6 @@ public function _testAlterInfo() {
* Test text_format translation.
*/
public function testTextFormatTranslation() {
- // Install the Filter Test module to access the provided text formats.
- $this->container->get('module_handler')->install(array('filter_test'));
$this->drupalLogin($this->admin_user);
/** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
$config_factory = $this->container->get('config.factory');
@@ -724,6 +735,35 @@ public function testTextFormatTranslation() {
->get('content');
// The translation should not have changed, so re-use $expected.
$this->assertEqual($expected, $actual);
+
+ // Because the text is now in a text format that the translator does not
+ // have access to, the translator should not be able to translate it.
+ $translation_page_url = "$translation_base_url/fr/edit";
+ $this->drupalLogin($this->translator_user);
+ $this->drupalGet($translation_page_url);
+ $this->assertDisabledTextarea('edit-translation-config-names-config-translation-testcontent-content-value');
+ $this->drupalPostForm(NULL, array(), t('Save translation'));
+ // Check that submitting the form did not update the text format of the
+ // translation.
+ $actual = $config_factory
+ ->get('config_translation_test.content')
+ ->get('content');
+ $this->assertEqual($expected, $actual);
+
+ // The administrator must explicitly change the text format.
+ $this->drupalLogin($this->admin_user);
+ $edit = array(
+ 'translation[config_names][config_translation_test.content][content][format]' => 'full_html',
+ );
+ $this->drupalPostForm($translation_page_url, $edit, t('Save translation'));
+ $expected = array(
+ 'value' => 'Hello World - FR',
+ 'format' => 'full_html',
+ );
+ $actual = $config_factory
+ ->get('config_translation_test.content')
+ ->get('content');
+ $this->assertEqual($expected, $actual);
}
/**
@@ -794,4 +834,33 @@ protected function renderContextualLinks($ids, $current_path) {
return $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => $current_path)));
}
+ /**
+ * Asserts that a textarea with a given ID has been disabled from editing.
+ *
+ * @param string $id
+ * The HTML ID of the textarea.
+ *
+ * @return bool
+ * TRUE if the assertion passed; FALSE otherwise.
+ */
+ protected function assertDisabledTextarea($id) {
+ $textarea = $this->xpath('//textarea[@id=:id and contains(@disabled, "disabled")]', array(
+ ':id' => $id,
+ ));
+ $textarea = reset($textarea);
+ $passed = $this->assertTrue($textarea instanceof \SimpleXMLElement, String::format('Disabled field @id exists.', array(
+ '@id' => $id,
+ )));
+ $expected = 'This field has been disabled because you do not have sufficient permissions to edit it.';
+ $passed = $passed && $this->assertEqual((string) $textarea, $expected, String::format('Disabled textarea @id hides text in an inaccessible text format.', array(
+ '@id' => $id,
+ )));
+ // Make sure the text format select is not shown.
+ $select_id = str_replace('value', 'format--2', $id);
+ $select = $this->xpath('//select[@id=:id]', array(':id' => $select_id));
+ return $passed && $this->assertFalse($select, String::format('Field @id does not exist.', array(
+ '@id' => $id,
+ )));
+ }
+
}
diff --git a/core/modules/locale/src/LocaleConfigSubscriber.php b/core/modules/locale/src/LocaleConfigSubscriber.php
index 4d97644..651651b 100644
--- a/core/modules/locale/src/LocaleConfigSubscriber.php
+++ b/core/modules/locale/src/LocaleConfigSubscriber.php
@@ -11,7 +11,6 @@
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigOverrideCrudEvent;
use Drupal\Core\Config\Schema\ArrayElement;
-use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\language\Config\LanguageConfigOverride;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -61,8 +60,8 @@ public function __construct(StringStorageInterface $string_storage, ConfigFactor
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
- // Instead of deleting the actual translation strings, we save empty strings
- // when the configuration override gets deleted so we can re-use the same
+ // Instead of deleting the actual translation strings we save empty strings
+ // when the configuration override gets deleted, so we can re-use the same
// function for both events.
$events[ConfigEvents::SAVE_OVERRIDE] = 'onOverrideUpdate';
$events[ConfigEvents::DELETE_OVERRIDE] = 'onOverrideUpdate';
@@ -111,7 +110,6 @@ public function onOverrideUpdate(ConfigOverrideCrudEvent $event) {
*/
protected function saveStrings(Config $source_config, LanguageConfigOverride $translation_config, ArrayElement $schema, $base_key = NULL) {
foreach ($schema as $key => $element) {
-
$element_key = implode('.', array_filter(array($base_key, $key)));
// We only care for strings here, so traverse the schema further in the