diff --git a/core/lib/Drupal/Core/Form/FormErrorHandler.php b/core/lib/Drupal/Core/Form/FormErrorHandler.php index df736b0..2052256 100644 --- a/core/lib/Drupal/Core/Form/FormErrorHandler.php +++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php @@ -83,8 +83,10 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st } elseif ($is_visible_element && $has_title && $has_id) { // We need to pass this through SafeMarkup::escape() so - // drupal_set_message() does not encode the links. - $error_links[] = SafeMarkup::escape($this->l($title, Url::fromRoute('', [], ['fragment' => $form_element['#id'], 'external' => TRUE]))); + // drupal_set_message() does not encode the links. The element title, + // however, can contain HTML so we use a markup element array so that at + // least malicious tags are stripped. + $error_links[] = SafeMarkup::escape($this->l(['#markup' => $title], Url::fromRoute('', [], ['fragment' => $form_element['#id'], 'external' => TRUE]))); unset($errors[$name]); } } diff --git a/core/modules/config_translation/src/FormElement/FormElementBase.php b/core/modules/config_translation/src/FormElement/FormElementBase.php index e13cc31..93be430 100644 --- a/core/modules/config_translation/src/FormElement/FormElementBase.php +++ b/core/modules/config_translation/src/FormElement/FormElementBase.php @@ -8,6 +8,7 @@ namespace Drupal\config_translation\FormElement; use Drupal\Core\Config\Config; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\TypedData\TypedDataInterface; @@ -147,6 +148,12 @@ protected function getSourceElement(LanguageInterface $source_language, $source_ * translation of complex data, similar access logic must be implemented * manually. * + * Note that because configuration values might be output on the page the form + * element is also responsible for validating that the input does not contain + * malicious HTML input. FormElementBase::validateInput(), which uses + * locale_string_is_safe() for the validation, is provided for that purpose + * and is used for the default form element. + * * @param \Drupal\Core\Language\LanguageInterface $translation_language * The language to display the translation form for. * @param mixed $source_config @@ -159,6 +166,8 @@ protected function getSourceElement(LanguageInterface $source_language, $source_ * * @see \Drupal\config_translation\FormElement\TextFormat * @see filter_process_format() + * @see \Drupal\config_translation\FormElement\FormElementBase::validateInput() + * @see locale_string_is_safe() */ protected function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) { // Add basic properties that apply to all form elements. @@ -170,10 +179,25 @@ protected function getTranslationElement(LanguageInterface $translation_language )), '#default_value' => $translation_config, '#attributes' => array('lang' => $translation_language->getId()), + '#element_validate' => [[get_class($this), 'validateInput']], ); } /** + * Validates that the form element does not contain malicious HTML input. + * + * @param array $element + * The form element to validate. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public static function validateInput(array $element, FormStateInterface $form_state) { + if (isset($element['#value']) && !locale_string_is_safe($element['#value'])) { + $form_state->setError($element, t('The submitted string contains disallowed HTML.')); + } + } + + /** * {@inheritdoc} */ public function setConfig(Config $base_config, LanguageConfigOverride $config_translation, $config_values, $base_key = NULL) { diff --git a/core/modules/system/src/Tests/Element/PathElementFormTest.php b/core/modules/system/src/Tests/Element/PathElementFormTest.php index 5698951..758fcb6 100644 --- a/core/modules/system/src/Tests/Element/PathElementFormTest.php +++ b/core/modules/system/src/Tests/Element/PathElementFormTest.php @@ -11,6 +11,7 @@ use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element\PathElement; +use Drupal\Core\Render\RenderContext; use Drupal\Core\Url; use Drupal\simpletest\KernelTestBase; use Drupal\user\Entity\Role; @@ -147,6 +148,9 @@ public function validateForm(array &$form, FormStateInterface $form_state) {} * Tests that default handlers are added even if custom are specified. */ public function testPathElement() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = $this->container->get('renderer'); + $form_state = (new FormState()) ->setValues([ 'required_validate' => 'user/' . $this->testUser->id(), @@ -155,7 +159,9 @@ public function testPathElement() { 'required_validate_url' => 'user/' . $this->testUser->id(), ]); $form_builder = $this->container->get('form_builder'); - $form_builder->submitForm($this, $form_state); + $renderer->executeInRenderContext(new RenderContext(), function () use ($form_builder, &$form_state) { + $form_builder->submitForm($this, $form_state); + }); // Valid form state. $this->assertEqual(count($form_state->getErrors()), 0); @@ -180,7 +186,9 @@ public function testPathElement() { 'required_validate_route' => 'user/' . $this->testUser->id(), 'required_validate_url' => 'user/' . $this->testUser->id(), ]); - $form_builder->submitForm($this, $form_state); + $renderer->executeInRenderContext(new RenderContext(), function () use ($form_builder, &$form_state) { + $form_builder->submitForm($this, $form_state); + }); $errors = $form_state->getErrors(); // Should be missing 'required_validate' field. $this->assertEqual(count($errors), 1); @@ -195,7 +203,9 @@ public function testPathElement() { 'required_validate_url' => 'user/74', ]); $form_builder = $this->container->get('form_builder'); - $form_builder->submitForm($this, $form_state); + $renderer->executeInRenderContext(new RenderContext(), function () use ($form_builder, &$form_state) { + $form_builder->submitForm($this, $form_state); + }); // Valid form state. $errors = $form_state->getErrors(); diff --git a/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php b/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php index 22622e7..befcefd 100644 --- a/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php +++ b/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php @@ -8,6 +8,7 @@ namespace Drupal\system\Tests\Form; use Drupal\Core\Form\FormState; +use Drupal\Core\Render\RenderContext; use Drupal\simpletest\WebTestBase; use Drupal\Core\Form\FormStateInterface; @@ -227,7 +228,11 @@ private function formSubmitHelper($form, $edit) { \Drupal::formBuilder()->prepareForm($form_id, $form, $form_state); - \Drupal::formBuilder()->processForm($form_id, $form, $form_state); + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = $this->container->get('renderer'); + $renderer->executeInRenderContext(new RenderContext(), function () use ($form_id, &$form, &$form_state) { + \Drupal::formBuilder()->processForm($form_id, $form, $form_state); + }); $errors = $form_state->getErrors(); diff --git a/core/modules/system/src/Tests/Form/FormTest.php b/core/modules/system/src/Tests/Form/FormTest.php index e556b20..c754957 100644 --- a/core/modules/system/src/Tests/Form/FormTest.php +++ b/core/modules/system/src/Tests/Form/FormTest.php @@ -11,6 +11,7 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Form\FormState; use Drupal\Core\Render\Element; +use Drupal\Core\Render\RenderContext; use Drupal\form_test\Form\FormTestDisabledElementsForm; use Drupal\simpletest\WebTestBase; use Drupal\user\RoleInterface; @@ -118,13 +119,17 @@ function testRequiredFields() { // so we bypass it by setting the token to FALSE. $form['#token'] = FALSE; \Drupal::formBuilder()->prepareForm($form_id, $form, $form_state); - \Drupal::formBuilder()->processForm($form_id, $form, $form_state); + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = $this->container->get('renderer'); + $renderer->executeInRenderContext(new RenderContext(), function () use ($form_id, &$form, &$form_state) { + \Drupal::formBuilder()->processForm($form_id, $form, $form_state); + }); $errors = $form_state->getErrors(); // Form elements of type 'radios' throw all sorts of PHP notices // when you try to render them like this, so we ignore those for // testing the required marker. // @todo Fix this work-around (https://www.drupal.org/node/588438). - $form_output = ($type == 'radios') ? '' : \Drupal::service('renderer')->renderRoot($form); + $form_output = ($type == 'radios') ? '' : $renderer->renderRoot($form); if ($required) { // Make sure we have a form error for this element. $this->assertTrue(isset($errors[$element]), "Check empty($key) '$type' field '$element'"); diff --git a/core/modules/system/src/Tests/Form/ProgrammaticTest.php b/core/modules/system/src/Tests/Form/ProgrammaticTest.php index b138d16..b810d0e 100644 --- a/core/modules/system/src/Tests/Form/ProgrammaticTest.php +++ b/core/modules/system/src/Tests/Form/ProgrammaticTest.php @@ -8,6 +8,7 @@ namespace Drupal\system\Tests\Form; use Drupal\Core\Form\FormState; +use Drupal\Core\Render\RenderContext; use Drupal\simpletest\WebTestBase; /** @@ -73,7 +74,11 @@ function testSubmissionWorkflow() { private function submitForm($values, $valid_input) { // Programmatically submit the given values. $form_state = (new FormState())->setValues($values); - \Drupal::formBuilder()->submitForm('\Drupal\form_test\Form\FormTestProgrammaticForm', $form_state); + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = $this->container->get('renderer'); + $renderer->executeInRenderContext(new RenderContext(), function () use (&$form_state) { + \Drupal::formBuilder()->submitForm('\Drupal\form_test\Form\FormTestProgrammaticForm', $form_state); + }); // Check that the form returns an error when expected, and vice versa. $errors = $form_state->getErrors(); diff --git a/core/modules/system/src/Tests/Form/TriggeringElementProgrammedUnitTest.php b/core/modules/system/src/Tests/Form/TriggeringElementProgrammedUnitTest.php index f7cffa1..f190bc0 100644 --- a/core/modules/system/src/Tests/Form/TriggeringElementProgrammedUnitTest.php +++ b/core/modules/system/src/Tests/Form/TriggeringElementProgrammedUnitTest.php @@ -10,6 +10,7 @@ use Drupal\Core\Form\FormInterface; use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\RenderContext; use Drupal\simpletest\KernelTestBase; /** @@ -90,7 +91,11 @@ function testLimitValidationErrors() { $form_state = new FormState(); $form_state->setValue('section', 'one'); $form_builder = $this->container->get('form_builder'); - $form_builder->submitForm($this, $form_state); + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = $this->container->get('renderer'); + $renderer->executeInRenderContext(new RenderContext(), function () use ($form_builder, &$form_state) { + $form_builder->submitForm($this, $form_state); + }); // Verify that only the specified section was validated. $errors = $form_state->getErrors(); diff --git a/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php b/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php index 53d7e76..86367ef 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Form; +use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Form\FormState; use Drupal\Tests\UnitTestCase; @@ -24,7 +25,11 @@ public function testDisplayErrorMessages() { $link_generator = $this->getMock('Drupal\Core\Utility\LinkGeneratorInterface'); $link_generator->expects($this->any()) ->method('generate') - ->willReturnArgument(0); + ->willReturnCallback(function (array $build) { + // @todo This should not be necessary after + // https://www.drupal.org/node/2501975 + return SafeMarkup::set($build['#markup']); + }); $form_error_handler = $this->getMockBuilder('Drupal\Core\Form\FormErrorHandler') ->setConstructorArgs([$this->getStringTranslationStub(), $link_generator]) ->setMethods(['drupalSetMessage']) @@ -41,7 +46,7 @@ public function testDisplayErrorMessages() { ->with('this missing element is invalid', 'error'); $form_error_handler->expects($this->at(3)) ->method('drupalSetMessage') - ->with('3 errors have been found: Test 1, Test 2 & a half, Test 3', 'error'); + ->with('3 errors have been found: Test 1, Test 2 & a half, Test 3', 'error'); $form = [ '#parents' => [],