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 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: , ,
.";
+ $expected_filtered_text = "Text with some disallowed tags: , unicorn, .";
+ $this->assertEqual(check_markup($text, 'filtered_html', '', FALSE, array()), $expected_filtered_text, 'Expected filter result.');
+ $this->assertEqual(check_markup($text, 'filtered_html', '', FALSE, array(FILTER_TYPE_HTML_RESTRICTOR)), $expected_filtered_text, 'Expected filter result, even when trying to disable filters of the FILTER_TYPE_HTML_RESTRICTOR type.');
+ }
}
diff --git a/core/modules/php/php.module b/core/modules/php/php.module
index 73db6d0..8e885e4 100644
--- a/core/modules/php/php.module
+++ b/core/modules/php/php.module
@@ -138,6 +138,7 @@ function _php_filter_tips($filter, $format, $long = FALSE) {
function php_filter_info() {
$filters['php_code'] = array(
'title' => t('PHP evaluator'),
+ 'type' => FILTER_TYPE_MARKUP_LANGUAGE,
'description' => t('Executes a piece of PHP code. The usage of this filter should be restricted to administrators only!'),
'process callback' => 'php_eval',
'tips callback' => '_php_filter_tips',
diff --git a/core/modules/system/tests/modules/filter_test/filter_test.module b/core/modules/system/tests/modules/filter_test/filter_test.module
index 2cebc70..a61941a 100644
--- a/core/modules/system/tests/modules/filter_test/filter_test.module
+++ b/core/modules/system/tests/modules/filter_test/filter_test.module
@@ -32,11 +32,13 @@ function filter_test_filter_format_disable($format) {
function filter_test_filter_info() {
$filters['filter_test_uncacheable'] = array(
'title' => 'Uncacheable filter',
+ 'type' => FILTER_TYPE_TRANSFORM_IRREVERSIBLE,
'description' => 'Does nothing, but makes a text format uncacheable.',
'cache' => FALSE,
);
$filters['filter_test_replace'] = array(
'title' => 'Testing filter',
+ 'type' => FILTER_TYPE_TRANSFORM_IRREVERSIBLE,
'description' => 'Replaces all content with filter and text format information.',
'process callback' => 'filter_test_replace',
);