diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index cb17c44..37649d8 100644 --- a/core/modules/filter/filter.module +++ b/core/modules/filter/filter.module @@ -441,8 +441,11 @@ function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE, * The form element to process. Properties used: * - #base_type: The form element #type to use for the 'value' element. * 'textarea' by default. - * - #format: (optional) The text format ID to preselect. If NULL or not set, - * the default format for the current user will be used. + * - #format: (optional) The text format ID to preselect. If omitted, the + * default format for the current user will be used. + * - #allowed_formats: (optional) An array of text format IDs that are + * available for this element. If omitted, all text formats that the current + * user has access to will be allowed. * * @return * The expanded element. @@ -481,6 +484,8 @@ function filter_process_format($element) { $element['value']['#type'] = $element['#base_type']; $element['value'] += element_info($element['#base_type']); + // Make sure the #default_value key is set, so we can use it below. + $element['value'] += array('#default_value' => ''); // Turn original element into a text format wrapper. $element['#attached']['library'][] = 'filter/drupal.filter'; @@ -494,15 +499,30 @@ function filter_process_format($element) { // Get a list of formats that the current user has access to. $formats = filter_formats($user); - // Use the default format for this user if none was selected. - if (!isset($element['#format'])) { - $element['#format'] = filter_default_format($user); + // Allow the list of formats to be restricted. + if (isset($element['#allowed_formats'])) { + // We do not add the fallback format here to allow the use-case of forcing + // certain text formats to be used for certain text areas. In case the + // fallback format is supposed to be allowed as well, it must be added to + // $element['#allowed_formats'] explicitly. + $formats = array_intersect_key($formats, array_flip($element['#allowed_formats'])); } - // If multiple text formats are available, remove the fallback. The - // "always_show_fallback_choice" is a hidden variable that has no UI. It - // defaults to false. - if (!\Drupal::config('filter.settings')->get('always_show_fallback_choice')) { + if (!isset($element['#format']) && !empty($formats)) { + // If no text format was selected, use the allowed format with the highest + // weight. This is equivalent to calling filter_default_format(). + $element['#format'] = reset($formats)->format; + } + + // If #allowed_formats is set, the list of formats must not be modified in any + // way. Otherwise, however, if all of the following conditions are true, + // remove the fallback format from the list of formats: + // 1. The 'always_show_fallback_choice' filter setting has not been activated. + // 2. Multiple text formats are available. + // 3. The fallback format is not the default format. + // The 'always_show_fallback_choice' filter setting is a hidden setting that + // has no UI. It defaults to FALSE. + if (!isset($element['#allowed_formats']) && !\Drupal::config('filter.settings')->get('always_show_fallback_choice')) { $fallback_format = filter_fallback_format(); if ($element['#format'] !== $fallback_format && count($formats) > 1) { unset($formats[$fallback_format]); @@ -544,12 +564,13 @@ function filter_process_format($element) { $all_formats = filter_formats(); $format_exists = isset($all_formats[$element['#format']]); + $format_allowed = !isset($element['#allowed_formats']) || in_array($element['#format'], $element['#allowed_formats']); $user_has_access = isset($formats[$element['#format']]); $user_is_admin = user_access('administer filters'); - // If the stored format does not exist, administrators have to assign a new - // format. - if (!$format_exists && $user_is_admin) { + // If the stored format does not exist or if it is not among the allowed + // formats for this textarea, administrators have to assign a new format. + if ((!$format_exists || !$format_allowed) && $user_is_admin) { $element['format']['format']['#required'] = TRUE; $element['format']['format']['#default_value'] = NULL; // Force access to the format selector (it may have been denied above if diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php index 1c33d74..29874b7 100644 --- a/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php @@ -24,7 +24,7 @@ class FilterAPITest extends EntityUnitTestBase { public static function getInfo() { return array( - 'name' => 'API', + 'name' => 'Filter API', 'description' => 'Test the behavior of the API of the Filter module.', 'group' => 'Filter', ); @@ -34,36 +34,6 @@ function setUp() { parent::setUp(); $this->installConfig(array('system', 'filter')); - - // Create Filtered HTML format. - $filtered_html_format = entity_create('filter_format', array( - 'format' => 'filtered_html', - 'name' => 'Filtered HTML', - 'filters' => array( - // Note that the filter_html filter is of the type FilterInterface::TYPE_MARKUP_LANGUAGE. - 'filter_url' => array( - 'weight' => -1, - 'status' => 1, - ), - // Note that the filter_html filter is of the type FilterInterface::TYPE_HTML_RESTRICTOR. - 'filter_html' => array( - 'status' => 1, - 'settings' => array( - 'allowed_html' => '


', - ), - ), - ) - )); - $filtered_html_format->save(); - - // Create Full HTML format. - $full_html_format = entity_create('filter_format', array( - 'format' => 'full_html', - 'name' => 'Full HTML', - 'weight' => 1, - 'filters' => array(), - )); - $full_html_format->save(); } /** @@ -105,13 +75,17 @@ function testCheckMarkupFilterSubset() { $expected_filtered_text = "Text with evil content and a URL: http://drupal.org!"; $expected_filter_text_without_html_generators = "Text with evil content and a URL: http://drupal.org!"; + $actual_filtered_text = check_markup($text, 'filtered_html', '', FALSE, array()); + $this->verbose("Actual:

$actual_filtered_text
Expected:
$expected_filtered_text
"); $this->assertIdentical( - check_markup($text, 'filtered_html', '', FALSE, array()), + $actual_filtered_text, $expected_filtered_text, 'Expected filter result.' ); + $actual_filtered_text_without_html_generators = check_markup($text, 'filtered_html', '', FALSE, array(FilterInterface::TYPE_MARKUP_LANGUAGE)); + $this->verbose("Actual:
$actual_filtered_text_without_html_generators
Expected:
$expected_filter_text_without_html_generators
"); $this->assertIdentical( - check_markup($text, 'filtered_html', '', FALSE, array(FilterInterface::TYPE_MARKUP_LANGUAGE)), + $actual_filtered_text_without_html_generators, $expected_filter_text_without_html_generators, 'Expected filter result when skipping FilterInterface::TYPE_MARKUP_LANGUAGE filters.' ); @@ -119,8 +93,10 @@ function testCheckMarkupFilterSubset() { // this check focuses on the ability to filter multiple filter types at once. // Drupal core only ships with these two types of filters, so this is the // most extensive test possible. + $actual_filtered_text_without_html_generators = check_markup($text, 'filtered_html', '', FALSE, array(FilterInterface::TYPE_HTML_RESTRICTOR, FilterInterface::TYPE_MARKUP_LANGUAGE)); + $this->verbose("Actual:
$actual_filtered_text_without_html_generators
Expected:
$expected_filter_text_without_html_generators
"); $this->assertIdentical( - check_markup($text, 'filtered_html', '', FALSE, array(FilterInterface::TYPE_HTML_RESTRICTOR, FilterInterface::TYPE_MARKUP_LANGUAGE)), + $actual_filtered_text_without_html_generators, $expected_filter_text_without_html_generators, 'Expected filter result when skipping FilterInterface::TYPE_MARKUP_LANGUAGE filters, even when trying to disable filters of the FilterInterface::TYPE_HTML_RESTRICTOR type.' ); diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php new file mode 100644 index 0000000..5aa9e65 --- /dev/null +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php @@ -0,0 +1,271 @@ + 'Text format form element', + 'description' => 'Tests form elements with associated text formats.', + 'group' => 'Filter', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + /** @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'); + + // Create users. + $this->adminUser = $this->drupalCreateUser(array( + 'administer filters', + $filtered_html_format->getPermissionName(), + $full_html_format->getPermissionName(), + $filter_test_format->getPermissionName(), + )); + + $this->webUser = $this->drupalCreateUser(array( + $filtered_html_format->getPermissionName(), + $filter_test_format->getPermissionName(), + )); + } + + /** + * Tests various different configurations of the 'text_format' element. + */ + public function testFilterForm() { + $this->doFilterFormTestAsAdmin(); + $this->doFilterFormTestAsNonAdmin(); + } + + /** + * Tests the behavior of the 'text_format' element as an administrator. + */ + protected function doFilterFormTestAsAdmin() { + $this->drupalLogin($this->adminUser); + $this->drupalGet('filter-test/text-format'); + + // Test a text format element with all formats. + $formats = array('filtered_html', 'full_html', 'filter_test'); + // If no default is given, the format with the lowest weight becomes the + // default. + $this->assertOptions('edit-all-formats-no-default-format--2', $formats, 'filtered_html'); + // \Drupal\filter_test\Form\FilterTestFormatForm::buildForm() uses + // 'filter_test' as the default value in this case. + $this->assertOptions('edit-all-formats-default-format--2', $formats, 'filter_test'); + // If a missing format is set as the default, administrators must select a + // valid replacement format. + $this->assertRequiredSelectAndOptions('edit-all-formats-default-missing-format--2', $formats); + + // Test a text format element with a predefined list of formats. + $formats = array('full_html', 'filter_test'); + $this->assertOptions('edit-restricted-formats-no-default-format--2', $formats, 'full_html'); + $this->assertOptions('edit-restricted-formats-default-format--2', $formats, 'full_html'); + $this->assertRequiredSelectAndOptions('edit-restricted-formats-default-missing-format--2', $formats); + $this->assertRequiredSelectAndOptions('edit-restricted-formats-default-disallowed-format--2', $formats); + + // Test a text format element with a fixed format. + $formats = array('filter_test'); + // When there is only a single option there is no point in choosing. + $this->assertNoSelect('edit-single-format-no-default-format--2'); + $this->assertNoSelect('edit-single-format-default-format--2'); + // If the select has a missing or disallowed format make the administrator + // explicitly choose the format. + $this->assertRequiredSelectAndOptions('edit-single-format-default-missing-format--2', $formats); + $this->assertRequiredSelectAndOptions('edit-single-format-default-disallowed-format--2', $formats); + } + + /** + * Tests the behavior of the 'text_format' element as a normal user. + */ + protected function doFilterFormTestAsNonAdmin() { + $this->drupalLogin($this->webUser); + $this->drupalGet('filter-test/text-format'); + + // Test a text format element with all formats. Only formats the user has + // access to are shown. + $formats = array('filtered_html', 'filter_test'); + // If no default is given, the format with the lowest weight becomes the + // default. This happens to be 'filtered_html'. + $this->assertOptions('edit-all-formats-no-default-format--2', $formats, 'filtered_html'); + // \Drupal\filter_test\Form\FilterTestFormatForm::buildForm() uses + // 'filter_test' as the default value in this case. + $this->assertOptions('edit-all-formats-default-format--2', $formats, 'filter_test'); + // If a missing format is given as default, administers must select a valid + // replacement format. + $this->assertDisabledTextarea('edit-all-formats-default-missing-value'); + + // Test a text format element with a predefined list of formats. + // The user only has access to the 'filter_test' format, so when no default + // is given that is preselected and the text format select is hidden. + $this->assertNoSelect('edit-restricted-formats-no-default-format--2'); + // When the format that the user does not have access to is preselected, the + // textarea should be disabled. + $this->assertDisabledTextarea('edit-restricted-formats-default-value'); + $this->assertDisabledTextarea('edit-restricted-formats-default-missing-value'); + $this->assertDisabledTextarea('edit-restricted-formats-default-disallowed-value'); + + // Test a text format element with a fixed format. + $formats = array('filtered_html'); + // When there is only a single option there is no point in choosing. + $this->assertNoSelect('edit-single-format-no-default-format--2'); + $this->assertNoSelect('edit-single-format-default-format--2'); + // If the select has a missing or disallowed format make sure the textarea + // is disabled. + $this->assertDisabledTextarea('edit-single-format-default-missing-value'); + $this->assertDisabledTextarea('edit-single-format-default-disallowed-value'); + } + + /** + * Makes sure that no select element with the given ID exists on the page. + * + * @param string $id + * The HTML ID of the select element. + */ + protected function assertNoSelect($id) { + $select = $this->xpath('//select[@id=:id]', array(':id' => $id)); + $this->assertFalse($select, String::format('Field @id does not exist.', array( + '@id' => $id, + ))); + return $select; + } + + /** + * Asserts that a select element has the correct options. + * + * @param string $id + * The HTML ID of the select element. + * @param array $expected_options + * An array of option values. + * @param string $selected + * The value of the selected option. + */ + protected function assertOptions($id, array $expected_options, $selected) { + $select = $this->xpath('//select[@id=:id]', array(':id' => $id)); + $select = reset($select); + $this->assertTrue($select instanceof \SimpleXMLElement, String::format('Field @id exists.', array( + '@id' => $id, + ))); + + $found_options = $this->getAllOptions($select); + foreach ($found_options as $found_key => $found_option) { + $expected_key = array_search($found_option->attributes()->value, $expected_options); + if ($expected_key !== FALSE) { + $this->pass(String::format('Option @option for field @id exists.', array( + '@option' => $expected_options[$expected_key], + '@id' => $id, + ))); + unset($found_options[$found_key]); + unset($expected_options[$expected_key]); + } + } + + // Make sure that all expected options were found and that there are no + // unexpected options. + foreach ($expected_options as $expected_option) { + $this->fail(String::format('Option @option for field @id exists.', array( + '@option' => $expected_option, + '@id' => $id, + ))); + } + foreach ($found_options as $found_option) { + $this->fail(String::format('Option @option for field @id does not exist.', array( + '@option' => $found_option->attributes()->value, + '@id' => $id, + ))); + } + + $this->assertOptionSelected($id, $selected); + } + + /** + * Asserts that there is a select element with the given ID that is required. + * + * @param string $id + * The HTML ID of the select element. + * @param array $options + * An array of option values that are contained in the select element + * besides the "- Select -" option. + */ + protected function assertRequiredSelectAndOptions($id, array $options) { + $select = $this->xpath('//select[@id=:id and contains(@required, "required")]', array( + ':id' => $id, + )); + $select = reset($select); + $this->assertTrue($select instanceof \SimpleXMLElement, String::format('Required field @id exists.', array( + '@id' => $id, + ))); + // A required select element has a "- Select -" option whose key is an empty + // string. + $options[] = ''; + $this->assertOptions($id, $options, ''); + } + + /** + * Asserts that a textarea with a given ID has been disabled from editing. + * + * @param string $id + * The HTML ID of the textarea. + */ + protected function assertDisabledTextarea($id) { + $textarea = $this->xpath('//textarea[@id=:id and contains(@disabled, "disabled")]', array( + ':id' => $id, + )); + $textarea = reset($textarea); + $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.'; + $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); + $this->assertNoSelect($select_id); + } + +} diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php index 6321c1b..3b8d114 100644 --- a/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php @@ -32,7 +32,7 @@ class FilterSecurityTest extends WebTestBase { public static function getInfo() { return array( - 'name' => 'Security', + 'name' => 'Filter security', 'description' => 'Test the behavior of check_markup() when a filter or text format vanishes, or when check_markup() is called in such a way that it is instructed to skip all filters of the "FilterInterface::TYPE_HTML_RESTRICTOR" type.', 'group' => 'Filter', ); @@ -44,19 +44,8 @@ function setUp() { // Create Basic page node type. $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); - // Create Filtered HTML format. - $filtered_html_format = entity_create('filter_format', array( - 'format' => 'filtered_html', - 'name' => 'Filtered HTML', - 'filters' => array( - // Note that the filter_html filter is of the type FilterInterface::TYPE_HTML_RESTRICTOR. - 'filter_html' => array( - 'status' => 1, - ), - ) - )); - $filtered_html_format->save(); - + /** @var \Drupal\filter\Entity\FilterFormat $filtered_html_format */ + $filtered_html_format = entity_load('filter_format', 'filtered_html'); $filtered_html_permission = $filtered_html_format->getPermissionName(); user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array($filtered_html_permission)); @@ -101,8 +90,8 @@ function testDisableFilterModule() { * Tests that security filters are enforced even when marked to be skipped. */ function testSkipSecurityFilters() { - $text = "Text with some disallowed tags: