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' => '
unicorn
',
- ),
- ),
- )
- ));
- $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: , , .";
- $expected_filtered_text = "Text with some disallowed tags: , unicorn, .";
+ $text = "Text with some disallowed tags: , ,
.";
+ $expected_filtered_text = "Text with some disallowed tags: ,