core/modules/ckeditor/js/ckeditor.js | 77 +++++++++- .../ckeditor/Plugin/CKEditorPlugin/Internal.php | 148 +++++++++++++++++++- .../lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php | 2 + .../lib/Drupal/ckeditor/Tests/CKEditorTest.php | 92 +++++++++++- core/modules/filter/filter.module | 148 ++++++++++++++++++++ .../lib/Drupal/filter/Plugin/Filter/FilterHtml.php | 15 ++ .../filter/Plugin/Filter/FilterHtmlEscape.php | 8 ++ .../filter/lib/Drupal/filter/Plugin/FilterBase.php | 7 + .../lib/Drupal/filter/Plugin/FilterInterface.php | 73 ++++++++++ .../lib/Drupal/filter/Tests/FilterAPITest.php | 104 ++++++++++++-- .../Filter/FilterTestRestrictTagsAndAttributes.php | 73 ++++++++++ 11 files changed, 727 insertions(+), 20 deletions(-) diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js index 447f916..895a5d1 100644 --- a/core/modules/ckeditor/js/ckeditor.js +++ b/core/modules/ckeditor/js/ckeditor.js @@ -6,6 +6,7 @@ Drupal.editors.ckeditor = { attach: function (element, format) { this._loadExternalPlugins(format); + this._ACF_HACK_to_support_blacklisted_attributes(element, format); return !!CKEDITOR.replace(element, format.editorSettings); }, @@ -42,6 +43,7 @@ Drupal.editors.ckeditor = { attachInlineEditor: function (element, format, mainToolbarId, floatedToolbarId) { this._loadExternalPlugins(format); + this._ACF_HACK_to_support_blacklisted_attributes(element, format); var settings = $.extend(true, {}, format.editorSettings); @@ -98,8 +100,81 @@ Drupal.editors.ckeditor = { } delete format.editorSettings.drupalExternalPlugins; } - } + }, + + /** + * This is a huge hack to do ONE thing: to allow Drupal to fully mandate what + * CKEditor should allow by setting CKEditor's allowedContent setting. The + * problem is that allowedContent only allows for whitelisting, whereas + * Drupal's default HTML filtering (the filter_html filter) also blacklists + * the "style" and "on*" ("onClick" etc.) attributes. + * + * So this function hacks in explicit support for Drupal's filter_html's need + * to blacklist specifically those attributes, until ACF supports blacklisting + * of properties: http://dev.ckeditor.com/ticket/10276. + * + * Limitations: + * - This does not support blacklisting of other attributes, it's only + * intended to implement filter_html's blacklisted attributes. + * - This is only a temporary work-around; it assumes the filter_html + * filter is being used whenever *any* restriction exists. This is a valid + * assumption for the default text formats in Drupal 8 core, but obviously + * won't work for release. + * + * This is the only way we could get https://drupal.org/node/1936392 committed + * before Drupal 8 code freeze on July 1, 2013. CKEditor has committed to + * explicitly supporting this in some way. + * + * @todo D8 remove this once http://dev.ckeditor.com/ticket/10276 is done. + */ + _ACF_HACK_to_support_blacklisted_attributes: function (element, format) { + function override(rule) { + var oldValue = rule.attributes; + function filter_html_override_attributes (attribute) { + // Disallow the "style" and "on*" attributes on any tag. + if (attribute === 'style' || attribute.substr(0, 2) === 'on') { + return false; + } + + // Ensure the original logic still runs, if any. + if (typeof oldValue === 'function') { + return oldValue(attribute); + } + else if (typeof oldValue === 'boolean') { + return oldValue; + } + + // Otherwise, accept this attribute. + return true; + } + rule.attributes = filter_html_override_attributes; + } + CKEDITOR.once('instanceLoaded', function(e) { + if (e.editor.name === element.id) { + // If everything is allowed, everything is allowed. + if (format.editorSettings.allowedContent === true) { + return; + } + // Otherwise, assume Drupal's filter_html filter is being used. + else { + // Get the filter object (ACF). + var filter = e.editor.filter; + // Find the "config" rule (the one caused by the allowedContent + // setting) for each HTML tag, and override its "attributes" value. + for (var el in filter._.rules.elements) { + if (filter._.rules.elements.hasOwnProperty(el)) { + for (var i = 0; i < filter._.rules.elements[el].length; i++) { + if (filter._.rules.elements[el][i].featureName === 'config') { + override(filter._.rules.elements[el][i]); + } + } + } + } + } + } + }); + } }; })(Drupal, CKEDITOR, jQuery); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php index e1f07d6..d206597 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php @@ -55,7 +55,11 @@ public function getConfig(Editor $editor) { ), ); - // Next, add the format_tags setting, if its button is enabled. + // Add the allowedContent setting, which ensures CKEditor only allows tags + // and attributes that are allowed by the text format for this text editor. + $config['allowedContent'] = $this->generateAllowedContentSetting($editor); + + // Add the format_tags setting, if its button is enabled. $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons'])); if (in_array('Format', $toolbar_buttons)) { $config['format_tags'] = $this->generateFormatTagsSetting($editor); @@ -242,6 +246,7 @@ public function getButtons() { * * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor * A configured text editor object. + * * @return array * An array containing the "format_tags" configuration. */ @@ -264,4 +269,145 @@ protected function generateFormatTagsSetting(Editor $editor) { return implode(';', $format_tags); } + /** + * Builds the "allowedContent" configuration part of the CKEditor JS settings. + * + * This ensures that CKEditor obeys the HTML restrictions defined by Drupal's + * filter system, by enabling CKEditor's Advanced Content Filter (ACF) + * functionality: http://ckeditor.com/blog/CKEditor-4.1-RC-Released. + * + * @see getConfig() + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * + * @return string|TRUE + * The "allowedContent" configuration: a well-formatted string or TRUE. The + * latter indicates that anything is allowed. + */ + protected function generateAllowedContentSetting(Editor $editor) { + // When nothing is disallowed, set allowedContent to true. + $filter_types = filter_get_filter_types_by_format($editor->format); + if (!in_array(FILTER_TYPE_HTML_RESTRICTOR, $filter_types)) { + return TRUE; + } + // Generate setting that accurately reflects allowed tags and attributes. + else { + $get_allowed_attribute_values = function($attribute_values) { + $values = array_keys(array_filter($attribute_values, function($value) { + return $value !== FALSE; + })); + if (count($values)) { + return implode(',', $values); + } + else { + return NULL; + } + }; + + $html_restrictions = filter_get_html_restrictions_by_format($editor->format); + // When all HTML is allowed, also set allowedContent to true. + if ($html_restrictions === FALSE) { + return TRUE; + } + $setting = array(); + foreach ($html_restrictions['allowedTags'] as $tag => $attributes) { + // Tell CKEditor the tag is allowed, but no attributes. + if ($attributes === FALSE) { + $setting[$tag] = array( + 'attributes' => FALSE, + 'styles' => FALSE, + 'classes' => FALSE, + ); + } + // Tell CKEditor the tag is allowed, as well as any attribute on it. + else if ($attributes === TRUE) { + $setting[$tag] = array( + 'attributes' => TRUE, + 'styles' => TRUE, + 'classes' => TRUE, + ); + // We've just marked that any value for the "style" and "class" + // attributes is allowed. However, that may not be the case: the "*" + // tag may still apply restrictions. + // Since CKEditor's ACF follows the following principle: + // Once validated, an element or its property cannot be + // invalidated by another rule. + // That means that the most permissive setting wins. Which means that + // it will still be allowed by CKEditor to e.g. define any style, no + // matter what the "*" tag's restrictions may be. If there's a setting + // for either the "style" or "class" attribute, it cannot possibly be + // more permissive than what was set above. Hence: inherit from the + // "*" tag where possible. + if (isset($html_restrictions['allowedTags']['*'])) { + $wildcard = $html_restrictions['allowedTags']['*']; + if (isset($wildcard['style'])) { + if (!is_array($wildcard['style'])) { + $setting[$tag]['styles'] = $wildcard['style']; + } + else { + $allowed_styles = $get_allowed_attribute_values($attributes['style']); + if (isset($allowed_values)) { + $setting[$tag]['styles'] = $allowed_styles; + } + else { + unset($setting[$tag]['styles']); + } + } + } + if (isset($wildcard['class'])) { + if (!is_array($wildcard['class'])) { + $setting[$tag]['classes'] = $wildcard['class']; + } + else { + $allowed_styles = $get_allowed_attribute_values($attributes['class']); + if (isset($allowed_values)) { + $setting[$tag]['classes'] = $allowed_styles; + } + else { + unset($setting[$tag]['classes']); + } + } + } + } + } + // Tell CKEditor the tag is allowed, along with some tags. + else if (is_array($attributes)) { + // CKEditor does not yet support blacklisting, so ignore those. + // @todo Update this once http://dev.ckeditor.com/ticket/10276 lands. + $attributes = array_filter($attributes, function($value) { + return $value !== FALSE; + }); + + // Configure allowed attributes, allowed "style" attribute values and + // allowed "class" attribute values. + // CKEditor only allows specific values for the "class" and "style" + // attributes; so ignore restrictions on other attributes, which + // Drupal filters may provide. + // NOTE: A Drupal contrib module can subclass this class, override the + // getConfig() method, and override the JavaScript at + // Drupal.editors.ckeditor to somehow make validation of values for + // attributes other than "class" and "style" work. + if (count($attributes)) { + $setting[$tag]['attributes'] = implode(',', array_keys($attributes)); + } + if (isset($attributes['style']) && is_array($attributes['style'])) { + $allowed_styles = $get_allowed_attribute_values($attributes['style']); + if (isset($allowed_values)) { + $setting[$tag]['styles'] = $allowed_styles; + } + } + if (isset($attributes['class']) && is_array($attributes['class'])) { + $allowed_classes = $get_allowed_attribute_values($attributes['class']); + if (isset($allowed_classes)) { + $setting[$tag]['classes'] = $allowed_classes; + } + } + } + } + + return $setting; + } + } + } diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php index c52636d..577ab74 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php @@ -143,6 +143,8 @@ public function getJSSettings(EditorEntity $editor) { 'drupalExternalPlugins' => array_map('file_create_url', $external_plugins), ); + ksort($settings); + return $settings; } diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php index 6f71077..f8fd5b7 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php @@ -21,7 +21,7 @@ class CKEditorTest extends DrupalUnitTestBase { * * @var array */ - public static $modules = array('system', 'editor', 'ckeditor'); + public static $modules = array('system', 'editor', 'ckeditor', 'filter_test'); /** * An instance of the "CKEditor" text editor plugin. @@ -53,6 +53,9 @@ function setUp() { 'filters' => array( 'filter_html' => array( 'status' => 1, + 'settings' => array( + 'allowed_html' => '
tag.
+ * 'p' => FALSE,
+ * // Allows the following attributes on the tag:
+ * // - 'href', with any value;
+ * // - 'rel', with the value 'nofollow' value.
+ * 'a' => array(
+ * 'href' => TRUE,
+ * 'rel' => array('nofollow' => TRUE),
+ * ),
+ * // Only allows the 'src' and 'alt' attributes on the
',
+ )
),
),
));
@@ -76,6 +79,7 @@ function testGetJSSettings() {
// Default toolbar.
$expected_config = $this->getDefaultInternalConfig() + array(
+ 'allowedContent' => $this->getDefaultAllowedContentConfig(),
'toolbar' => $this->getDefaultToolbarConfig(),
'contentsCss' => $this->getDefaultContentsCssConfig(),
'extraPlugins' => '',
@@ -83,6 +87,7 @@ function testGetJSSettings() {
'stylesSet' => FALSE,
'drupalExternalPlugins' => array(),
);
+ ksort($expected_config);
$this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for default configuration.');
// Customize the configuration: add button, have two contextually enabled
@@ -102,15 +107,79 @@ function testGetJSSettings() {
$expected_config['drupalExternalPlugins']['llama_contextual_and_button'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual_and_button.js');
$expected_config['contentsCss'][] = file_create_url('core/modules/ckeditor/tests/modules/ckeditor_test.css');
$expected_config['keystrokes'] = array(array(1114187, 'link'), array(1114188, NULL));
- $this->assertEqual($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
+ ksort($expected_config);
+ $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
- // Change the allowed HTML tags; the "format_tags" setting for CKEditor
- // should automatically be updated as well.
+ // Change the allowed HTML tags; the "allowedContent" and "format_tags"
+ // settings for CKEditor should automatically be updated as well.
$format = entity_load('filter_format', 'filtered_html');
$format->filters('filter_html')->settings['allowed_html'] .= '
';
$format->save();
+ $expected_config['allowedContent']['pre'] = array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE);
+ $expected_config['allowedContent']['h3'] = array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE);
$expected_config['format_tags'] = 'p;h3;h4;h5;h6;pre';
- $this->assertEqual($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
+ $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
+
+ // Disable the filter_html filter: allow *all *tags.
+ $format->setFilterConfig('filter_html', array('status' => 0));
+ $format->save();
+ $expected_config['allowedContent'] = TRUE;
+ $expected_config['format_tags'] = 'p;h1;h2;h3;h4;h5;h6;pre';
+ $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
+
+ // Enable the filter_test_restrict_tags_and_attributes filter.
+ $format->setFilterConfig('filter_test_restrict_tags_and_attributes', array(
+ 'status' => 1,
+ 'settings' => array(
+ 'restrictions' => array(
+ 'allowedTags' => array(
+ 'p' => TRUE,
+ 'a' => array(
+ 'href' => TRUE,
+ 'rel' => array('nofollow' => TRUE),
+ 'class' => array('external' => TRUE),
+ 'target' => array('_blank' => FALSE),
+ ),
+ 'span' => array(
+ 'class' => array('dodo' => FALSE),
+ 'property' => array('dc:*' => TRUE),
+ 'rel' => array('foaf:*' => FALSE),
+ ),
+ '*' => array(
+ 'style' => FALSE,
+ 'data-*' => TRUE,
+ ),
+ 'del' => FALSE,
+ )
+ ),
+ ),
+ ));
+ $format->save();
+ $expected_config['allowedContent'] = array(
+ 'p' => array(
+ 'attributes' => TRUE,
+ 'styles' => FALSE,
+ 'classes' => TRUE,
+ ),
+ 'a' => array(
+ 'attributes' => 'href,rel,class,target',
+ 'classes' => 'external',
+ ),
+ 'span' => array(
+ 'attributes' => 'class,property,rel',
+ ),
+ '*' => array(
+ 'attributes' => 'data-*',
+ ),
+ 'del' => array(
+ 'attributes' => FALSE,
+ 'styles' => FALSE,
+ 'classes' => FALSE,
+ ),
+ );
+ $expected_config['format_tags'] = 'p';
+ ksort($expected_config);
+ $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
}
/**
@@ -166,6 +235,7 @@ function testInternalGetConfig() {
// Default toolbar.
$expected = $this->getDefaultInternalConfig();
+ $expected['allowedContent'] = $this->getDefaultAllowedContentConfig();
$this->assertIdentical($expected, $internal_plugin->getConfig($editor), '"Internal" plugin configuration built correctly for default toolbar.');
// Format dropdown/button enabled: new setting should be present.
@@ -230,6 +300,18 @@ protected function getDefaultInternalConfig() {
);
}
+ protected function getDefaultAllowedContentConfig() {
+ return array(
+ 'h4' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
+ 'h5' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
+ 'h6' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
+ 'p' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
+ 'br' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
+ 'strong' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
+ 'a' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
+ );
+ }
+
protected function getDefaultToolbarConfig() {
return array(
0 => array('items' => array('Bold', 'Italic')),
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index cf0eaef..007509e 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -416,6 +416,154 @@ function filter_get_filter_types_by_format($format_id) {
}
/**
+ * Retrieve all HTML restrictions (tags and attributes) for a given text format.
+ *
+ * Note that restrictions applied to the "*" tag (the wildcard tag, i.e. all
+ * tags) are treated just like any other HTML tag. That means that any
+ * restrictions applied to it are not automatically applied to all other tags.
+ * It is up to the caller to handle this in whatever way it sees fit; this way
+ * no information granularity is lost.
+ *
+ * @param string $format_id
+ * A text format ID.
+ *
+ * @return array|FALSE
+ * An structured array as returned by FilterInterface::getHTMLRestrictions(),
+ * but with the intersection of all filters in this text format.
+ * Will either indicate blacklisting of tags or whitelisting of tags. In the
+ * latter case, it's possible that restrictions on attributes are also stored.
+ * FALSE means there are no HTML restrictions.
+ */
+function filter_get_html_restrictions_by_format($format_id) {
+ $format = filter_format_load($format_id);
+
+ // Ignore filters that are disabled or don't have HTML restrictions.
+ $filters = array_filter($format->filters()->getAll(), function($filter) {
+ if (!$filter->status) {
+ return FALSE;
+ }
+ if ($filter->getType() === FILTER_TYPE_HTML_RESTRICTOR && $filter->getHTMLRestrictions() !== FALSE) {
+ return TRUE;
+ }
+ return FALSE;
+ });
+
+ if (empty($filters)) {
+ return FALSE;
+ }
+ else {
+ // From the set of remaining filters (they were filtered by array_filter()
+ // above), collect the list of tags and attributes that are allowed by all
+ // filters, i.e. the intersection of all allowed tags and attributes.
+ $restrictions = array_reduce($filters, function($restrictions, $filter) {
+ $new_restrictions = $filter->getHTMLRestrictions();
+
+ // The first filter with HTML restrictions provides the initial set.
+ if (!isset($restrictions)) {
+ return $new_restrictions;
+ }
+ // Subsequent filters with an "allowed html" setting must be intersected
+ // with the existing set, to ensure we only end up with the tags that are
+ // allowed by *all* filters with an "allowed html" setting.
+ else {
+ // Track the union of forbidden (blacklisted) tags.
+ if (isset($new_restrictions['forbiddenTags'])) {
+ if (!isset($restrictions['forbiddenTags'])) {
+ $restrictions['forbiddenTags'] = $new_restrictions['forbiddenTags'];
+ }
+ else {
+ $restrictions['forbiddenTags'] = array_unique(array_merge($restrictions['forbiddenTags'], $new_restrictions['forbiddenTags']));
+ }
+ }
+
+ // Track the intersection of allowed (whitelisted) tags.
+ if (isset($restrictions['allowedTags'])) {
+ $intersection = $restrictions['allowedTags'];
+ foreach ($intersection as $tag => $attributes) {
+ // If the current tag is not whitelisted by the new filter, then
+ // it's outside of the intersection.
+ if (!array_key_exists($tag, $new_restrictions['allowedTags'])) {
+ // The exception is the asterisk (which applies to all tags): it
+ // does not need to be whitelisted by every filter in order to be
+ // used; not every filter needs attribute restrictions on all tags.
+ if ($tag === '*') {
+ continue;
+ }
+ unset($intersection[$tag]);
+ }
+ // The tag is in the intersection, but now we must calculate the
+ // intersection of the allowed attributes.
+ else {
+ $current_attributes = $intersection[$tag];
+ $new_attributes = $new_restrictions['allowedTags'][$tag];
+ // The current intersection does not allow any attributes, never
+ // allow.
+ if (!is_array($current_attributes) && $current_attributes == FALSE) {
+ continue;
+ }
+ // The new filter allows less attributes (all -> list or none).
+ else if (!is_array($current_attributes) && $current_attributes == TRUE && ($new_attributes == FALSE || is_array($new_attributes))) {
+ $intersection[$tag] = $new_attributes;
+ }
+ // The new filter allows less attributes (list -> none).
+ else if (is_array($current_attributes) && $new_attributes == FALSE) {
+ $intersection[$tag] = $new_attributes;
+ }
+ // The new filter allows more attributes; retain current.
+ else if (is_array($current_attributes) && $new_attributes == TRUE) {
+ continue;
+ }
+ // The new filter allows the same attributes; retain current.
+ else if ($current_attributes == $new_attributes) {
+ continue;
+ }
+ // Both list an array of attribute values; do an intersection,
+ // where we take into account that a value of:
+ // - TRUE means the attribute value is allowed;
+ // - FALSE means the attribute value is forbidden;
+ // hence we keep the ANDed result.
+ else {
+ $intersection[$tag] = array_intersect_key($intersection[$tag], $new_attributes);
+ foreach (array_keys($intersection[$tag]) as $attribute_value) {
+ $intersection[$tag][$attribute_value] = $intersection[$tag][$attribute_value] && $new_attributes[$attribute_value];
+ }
+ }
+ }
+ }
+ $restrictions['allowedTags'] = $intersection;
+ }
+
+ return $restrictions;
+ }
+ }, NULL);
+
+ // Simplification: if we have both a (intersected) whitelist and a (unioned)
+ // blacklist, then remove any tags from the whitelist that also exist in the
+ // blacklist. Now the whitelist alone expresses all tag-level restrictions,
+ // and we can delete the blacklist.
+ if (isset($restrictions['allowedTags']) && isset($restrictions['forbiddenTags'])) {
+ foreach ($restrictions['forbiddenTags'] as $tag) {
+ if (isset($restrictions['allowedTags'][$tag])) {
+ unset($restrictions['allowedTags'][$tag]);
+ }
+ }
+ unset($restrictions['forbiddenTags']);
+ }
+
+ // Simplification: if the only remaining allowed tag is the asterisk (which
+ // contains attribute restrictions that apply to all tags), and only
+ // whitelisting filters were used, then effectively nothing is allowed.
+ if (isset($restrictions['allowedTags'])) {
+ if (count($restrictions['allowedTags']) === 1 && array_key_exists('*', $restrictions['allowedTags']) && !isset($restrictions['forbiddenTags'])) {
+ $restrictions['allowedTags'] = array();
+ }
+ }
+
+ return $restrictions;
+ }
+}
+
+/**
* 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
diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtml.php b/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtml.php
index c939c27..e3715f7 100644
--- a/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtml.php
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtml.php
@@ -63,6 +63,21 @@ public function process($text, $langcode, $cache, $cache_id) {
/**
* {@inheritdoc}
*/
+ public function getHTMLRestrictions() {
+ $restrictions = array('allowedTags' => array());
+ $tags = preg_split('/\s+|<|>/', $this->settings['allowed_html'], -1, PREG_SPLIT_NO_EMPTY);
+ // List the allowed HTML tags.
+ foreach ($tags as $tag) {
+ $restrictions['allowedTags'][$tag] = TRUE;
+ }
+ // The 'style' and 'on*' ('onClick' etc.) attributes are always forbidden.
+ $restrictions['allowedTags']['*'] = array('style' => FALSE, 'on*' => FALSE);
+ return $restrictions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function tips($long = FALSE) {
global $base_url;
diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtmlEscape.php b/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtmlEscape.php
index f4a3694..ea05822 100644
--- a/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtmlEscape.php
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtmlEscape.php
@@ -34,6 +34,14 @@ public function process($text, $langcode, $cache, $cache_id) {
/**
* {@inheritdoc}
*/
+ public function getHTMLRestrictions() {
+ // Nothing is allowed.
+ return array('allowedTags' => array());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function tips($long = FALSE) {
return t('No HTML tags allowed.');
}
diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/FilterBase.php b/core/modules/filter/lib/Drupal/filter/Plugin/FilterBase.php
index ab56c91..2e80bd0 100644
--- a/core/modules/filter/lib/Drupal/filter/Plugin/FilterBase.php
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/FilterBase.php
@@ -150,6 +150,13 @@ public function prepare($text, $langcode, $cache, $cache_id) {
/**
* {@inheritdoc}
*/
+ public function getHTMLRestrictions() {
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function tips($long = FALSE) {
}
diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php b/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php
index 0857139..fd93ced 100644
--- a/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php
@@ -176,6 +176,79 @@ public function prepare($text, $langcode, $cache, $cache_id);
public function process($text, $langcode, $cache, $cache_id);
/**
+ * Returns HTML allowed by this filter's configuration.
+ *
+ * May be implemented by filters of the type FILTER_TYPE_HTML_RESTRICTOR, this
+ * won't be used for filters of other types; they should just return FALSE.
+ *
+ * This callback function is only necessary for filters that strip away HTML
+ * tags (and possibly attributes) and allows other modules to gain insight in
+ * a generic manner into which HTML tags and attributes are allowed by a
+ * format.
+ *
+ * @return array|FALSE
+ * A nested array with the allowed tags as keys, and for each of those tags
+ * (keys) the corresponding allowed attributes. An empty array for allowed
+ * attributes means no attributes are allowed, TRUE means all attributes are
+ * allowed. An example:
+ *
+ * Here is a concrete example, for a very granular filter:
+ * @code
+ * array(
+ * 'allowedTags' => array(
+ * // Allows any attribute with any value on the