Commit f37f9fa7 authored by webchick's avatar webchick
Browse files

Issue #1782838 by Wim Leers, Lars Toomre, chx: Added WYSIWYG in core: round one — filter types.

parent adf0c697
......@@ -4,9 +4,30 @@
* @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().
*/
......@@ -549,6 +570,39 @@ function filter_default_format($account = NULL) {
return $format->format;
}
/**
* 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.
*
......@@ -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. <code>&lt;br&gt;</code> and <code>&lt;p&gt;</code>)'),
'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 &lt;img&gt; 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,
......
<?php
/**
* @file
* Definition of Drupal\filter\Tests\FilterAPITest.
*/
namespace Drupal\filter\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the behavior of Filter's API.
*/
class FilterAPITest extends WebTestBase {
public static function getInfo() {
return array(
'name' => '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 <marquee>evil content and</marquee> a URL: http://drupal.org!";
$expected_filtered_text = "Text with evil content and a URL: <a href=\"http://drupal.org\">http://drupal.org</a>!";
$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.'
);
}
}
......@@ -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, 'Node body not found.');
}
/**
* Tests that security filters are enforced even when marked to be skipped.
*/
function testSkipSecurityFilters() {
$text = "Text with some disallowed tags: <script />, <em><object>unicorn</object></em>, <i><table></i>.";
$expected_filtered_text = "Text with some disallowed tags: , <em>unicorn</em>, .";
$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.');
}
}
......@@ -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',
......
......@@ -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',
);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment