diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index ae18740..b55066f 100644 --- a/core/modules/filter/filter.module +++ b/core/modules/filter/filter.module @@ -4,10 +4,31 @@ * @file * Framework for handling the filtering of content. */ + use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Template\Attribute; /** + * Non-HTML markup language filters that generate HTML. + */ +const FILTER_TYPE_MARKUP_LANGUAGE = 0; + +/** + * HTML tag and attribute restricting filters. + */ +const FILTER_TYPE_HTML_RESTRICTOR = 1; + +/** + * Reversible transformation filters. + */ +const FILTER_TYPE_TRANSFORM_REVERSIBLE = 2; + +/** + * Irreversible transformation filters. + */ +const FILTER_TYPE_TRANSFORM_IRREVERSIBLE = 3; + +/** * Implements hook_cache_flush(). */ function filter_cache_flush() { @@ -550,6 +571,39 @@ function filter_default_format($account = NULL) { } /** + * Retrieves all filter types that are used in a given text format. + * + * @param string $format_id + * A text format ID. + * + * @return array + * All filter types used by filters of a given text format. + * + * @throws Exception + */ +function filter_get_filter_types_by_format($format_id) { + $filter_types = array(); + + $filters = filter_list_format($format_id); + + // Ignore filters that are disabled. + $filters = array_filter($filters, function($filter) { + return $filter->status; + }); + + $filters_info = filter_get_filters(); + foreach ($filters as $filter) { + if (!isset($filters_info[$filter->name]['type'])) { + throw new Exception(t('Filter %filter has no type specified.', array ('%filter' => $filter->name))); + } + + $filter_types[] = $filters_info[$filter->name]['type']; + } + + return array_unique($filter_types); +} + +/** * Returns the ID of the fallback text format that all users have access to. * * The fallback text format is a regular text format in every respect, except @@ -759,13 +813,18 @@ function filter_list_format($format_id) { * Boolean whether to cache the filtered output in the {cache_filter} table. * The caller may set this to FALSE when the output is already cached * elsewhere to avoid duplicate cache lookups and storage. + * @param array $filter_types_to_skip + * (optional) An array of filter types to skip, or an empty array (default) + * to skip no filter types. All of the format's filters will be applied, + * except for filters of the types that are marked to be skipped. + * FILTER_TYPE_HTML_RESTRICTOR is the only type that cannot be skipped. * * @return * The filtered text. * * @ingroup sanitization */ -function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) { +function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE, $filter_types_to_skip = array()) { if (!isset($format_id)) { $format_id = filter_fallback_format(); } @@ -775,6 +834,16 @@ function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) return ''; } + // Prevent FILTER_TYPE_HTML_RESTRICTOR from being skipped. + if (in_array(FILTER_TYPE_HTML_RESTRICTOR, $filter_types_to_skip)) { + $filter_types_to_skip = array_diff($filter_types_to_skip, array(FILTER_TYPE_HTML_RESTRICTOR)); + } + + // When certain filters should be skipped, don't perform caching. + if ($filter_types_to_skip) { + $cache = FALSE; + } + // Check for a cached version of this piece of text. $cache = $cache && !empty($format->cache); $cache_id = ''; @@ -795,6 +864,10 @@ function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) // Give filters the chance to escape HTML-like data such as code or formulas. foreach ($filters as $name => $filter) { + // If necessary, skip filters of a certain type. + if (in_array($filter_info[$name]['type'], $filter_types_to_skip)) { + continue; + } if ($filter->status && isset($filter_info[$name]['prepare callback'])) { $function = $filter_info[$name]['prepare callback']; $text = $function($text, $filter, $format, $langcode, $cache, $cache_id); @@ -803,6 +876,10 @@ function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) // Perform filtering. foreach ($filters as $name => $filter) { + // If necessary, skip filters of a certain type. + if (in_array($filter_info[$name]['type'], $filter_types_to_skip)) { + continue; + } if ($filter->status && isset($filter_info[$name]['process callback'])) { $function = $filter_info[$name]['process callback']; $text = $function($text, $filter, $format, $langcode, $cache, $cache_id); @@ -1225,6 +1302,7 @@ function theme_filter_guidelines($variables) { function filter_filter_info() { $filters['filter_html'] = array( 'title' => t('Limit allowed HTML tags'), + 'type' => FILTER_TYPE_HTML_RESTRICTOR, 'process callback' => '_filter_html', 'settings callback' => '_filter_html_settings', 'default settings' => array( @@ -1237,11 +1315,13 @@ function filter_filter_info() { ); $filters['filter_autop'] = array( 'title' => t('Convert line breaks into HTML (i.e. <br> and <p>)'), + 'type' => FILTER_TYPE_MARKUP_LANGUAGE, 'process callback' => '_filter_autop', 'tips callback' => '_filter_autop_tips', ); $filters['filter_url'] = array( 'title' => t('Convert URLs into links'), + 'type' => FILTER_TYPE_MARKUP_LANGUAGE, 'process callback' => '_filter_url', 'settings callback' => '_filter_url_settings', 'default settings' => array( @@ -1251,6 +1331,7 @@ function filter_filter_info() { ); $filters['filter_html_image_secure'] = array( 'title' => t('Restrict images to this site'), + 'type' => FILTER_TYPE_HTML_RESTRICTOR, 'description' => t('Disallows usage of <img> tag sources that are not hosted on this site by replacing them with a placeholder image.'), 'process callback' => '_filter_html_image_secure_process', 'tips callback' => '_filter_html_image_secure_tips', @@ -1259,11 +1340,13 @@ function filter_filter_info() { ); $filters['filter_htmlcorrector'] = array( 'title' => t('Correct faulty and chopped off HTML'), + 'type' => FILTER_TYPE_HTML_RESTRICTOR, 'process callback' => '_filter_htmlcorrector', 'weight' => 10, ); $filters['filter_html_escape'] = array( 'title' => t('Display any HTML as plain text'), + 'type' => FILTER_TYPE_HTML_RESTRICTOR, 'process callback' => '_filter_html_escape', 'tips callback' => '_filter_html_escape_tips', 'weight' => -10, diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php new file mode 100644 index 0000000..e355f2d --- /dev/null +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php @@ -0,0 +1,111 @@ + 'API', + 'description' => 'Test the behavior of the API of the Filter module.', + 'group' => 'Filter', + ); + } + + function setUp() { + parent::setUp(); + + // Create Filtered HTML format. + $filtered_html_format = array( + 'format' => 'filtered_html', + 'name' => 'Filtered HTML', + 'filters' => array( + // Note that the filter_html filter is of the type FILTER_TYPE_MARKUP_LANGUAGE. + 'filter_url' => array( + 'weight' => -1, + 'status' => 1, + ), + // Note that the filter_html filter is of the type FILTER_TYPE_HTML_RESTRICTOR. + 'filter_html' => array( + 'status' => 1, + ), + ) + ); + $filtered_html_format = (object) $filtered_html_format; + filter_format_save($filtered_html_format); + + // Create Full HTML format. + $full_html_format = array( + 'format' => 'full_html', + 'name' => 'Full HTML', + 'weight' => 1, + 'filters' => array( + 'filter_htmlcorrector' => array( + 'weight' => 10, + 'status' => 1, + ), + ), + ); + $full_html_format = (object) $full_html_format; + filter_format_save($full_html_format); + } + + /** + * Tests the ability to apply only a subset of filters. + */ + function testCheckMarkup() { + $text = "Text with evil content and a URL: http://drupal.org!"; + $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!"; + + $this->assertIdentical( + check_markup($text, 'filtered_html', '', FALSE, array()), + $expected_filtered_text, + 'Expected filter result.' + ); + $this->assertIdentical( + check_markup($text, 'filtered_html', '', FALSE, array(FILTER_TYPE_MARKUP_LANGUAGE)), + $expected_filter_text_without_html_generators, + 'Expected filter result when skipping FILTER_TYPE_MARKUP_LANGUAGE filters.' + ); + // Related to @see FilterSecurityTest.php/testSkipSecurityFilters(), but + // 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. + $this->assertIdentical( + check_markup($text, 'filtered_html', '', FALSE, array(FILTER_TYPE_HTML_RESTRICTOR, FILTER_TYPE_MARKUP_LANGUAGE)), + $expected_filter_text_without_html_generators, + 'Expected filter result when skipping FILTER_TYPE_MARKUP_LANGUAGE filters, even when trying to disable filters of the FILTER_TYPE_HTML_RESTRICTOR type.' + ); + } + + /** + * Tests the function filter_get_filter_types_by_format(). + */ + function testFilterFormatAPI() { + // Test on filtered_html. + $this->assertEqual( + filter_get_filter_types_by_format('filtered_html'), + array(FILTER_TYPE_HTML_RESTRICTOR, FILTER_TYPE_MARKUP_LANGUAGE), + 'filter_get_filter_types_by_format() works as expected for the filtered_html format.' + ); + + // Test on full_html. + $this->assertEqual( + filter_get_filter_types_by_format('full_html'), + array(FILTER_TYPE_HTML_RESTRICTOR), + 'filter_get_filter_types_by_format() works as expected for the full_html format.' + ); + } + +} diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php index 9c2c46c..006bfe3 100644 --- a/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php @@ -24,7 +24,7 @@ class FilterSecurityTest extends WebTestBase { public static function getInfo() { return array( 'name' => 'Security', - 'description' => 'Test the behavior of check_markup() when a filter or text format vanishes.', + '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 "FILTER_TYPE_HTML_RESTRICTOR" type.', 'group' => 'Filter', ); } @@ -39,6 +39,12 @@ function setUp() { $filtered_html_format = array( 'format' => 'filtered_html', 'name' => 'Filtered HTML', + 'filters' => array( + // Note that the filter_html filter is of the type FILTER_TYPE_HTML_RESTRICTOR. + 'filter_html' => array( + 'status' => 1, + ), + ) ); $filtered_html_format = (object) $filtered_html_format; filter_format_save($filtered_html_format); @@ -82,4 +88,14 @@ function testDisableFilterModule() { $this->drupalGet('node/' . $node->nid); $this->assertNoText($body_raw, t('Node body not found.')); } + + /** + * Tests that security filters are enforced even when marked to be skipped. + */ + function testSkipSecurityFilters() { + $text = "Text with some disallowed tags: