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