 core/modules/ckeditor/js/ckeditor.js               |   77 ++++++++++-
 .../ckeditor/Plugin/CKEditorPlugin/Internal.php    |  137 ++++++++++++++++++-
 .../lib/Drupal/ckeditor/Tests/CKEditorTest.php     |   30 ++++-
 core/modules/filter/filter.module                  |  142 ++++++++++++++++++++
 .../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   |   68 ++++++++++
 .../lib/Drupal/filter/Tests/FilterAPITest.php      |   99 ++++++++++++--
 .../Filter/FilterTestRestrictTagsAndAttributes.php |   41 ++++++
 10 files changed, 609 insertions(+), 15 deletions(-)

diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js
index 447f916..56a447c 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 blacklisting 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 4516762..a0d0642 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php
@@ -56,7 +56,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);
@@ -243,6 +247,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.
    */
@@ -265,4 +270,134 @@ 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 {
+      $is_not_forbidden = function($value) {
+        return $value !== FALSE;
+      };
+      $get_allowed_attribute_values = function() {
+        $values = array_keys(array_filter($attribute_values, $is_not_forbidden));
+        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) {
+        if ($attributes === TRUE) {
+          // Tell CKEditor anything is allowed!
+          $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']);
+                }
+              }
+            }
+          }
+        }
+        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, $is_not_forbidden);
+
+          // 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 any other restrictions.
+          // 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['style']);
+            if (isset($allowed_classes)) {
+              $setting[$tag]['classes'] = $allowed_classes;
+            }
+          }
+        }
+      }
+
+      return $setting;
+    }
+  }
+
 }
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php
index 6f71077..70640db 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php
@@ -53,6 +53,9 @@ function setUp() {
       'filters' => array(
         'filter_html' => array(
           'status' => 1,
+          'settings' => array(
+            'allowed_html' => '<h4> <h5> <h6> <p> <br> <strong> <a>',
+          )
         ),
       ),
     ));
@@ -76,6 +79,7 @@ function testGetJSSettings() {
 
     // Default toolbar.
     $expected_config = $this->getDefaultInternalConfig() + array(
+      'allowedContent' => $this->getDefaultAllowedContentConfig(),
       'toolbar' => $this->getDefaultToolbarConfig(),
       'contentsCss' => $this->getDefaultContentsCssConfig(),
       'extraPlugins' => '',
@@ -104,13 +108,22 @@ function testGetJSSettings() {
     $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.');
 
-    // 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'] .= '<pre> <h3>';
     $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.');
+
+    // Remove the filtered_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->assertEqual($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
   }
 
   /**
@@ -166,6 +179,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 +244,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 99461e6..c260260 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -416,6 +416,148 @@ function filter_get_filter_types_by_format($format_id) {
 }
 
 /**
+ * Retrieve all HTML restrictions (tags and attributes) for a given text format.
+ *
+ * @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 most 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 bcfd15a..d09ddcf 100644
--- a/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtml.php
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtml.php
@@ -68,6 +68,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..92c5ebd 100644
--- a/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php
@@ -176,6 +176,74 @@ 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:
+   *     array(
+   *       'allowedTags' => array(
+   *         // Allows any attribute with any value on the <div> tag.
+   *         'div' => TRUE,
+   *         // Allows no attributes on the <p> tag.
+   *         'p' => FALSE,
+   *         // Allows the following attributes on the <a> 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 <alt> tag,
+   *         // with any value.
+   *         'img' => array(
+   *           'src' => TRUE,
+   *           'alt' => TRUE,
+   *         ),
+   *         // Allow RDFa on <span> tags, using only the dc, foaf,xsd and sioc
+   *         // vocabularies/namespaces.
+   *         'span' => array(
+   *           'property' => array('dc:*' => TRUE, 'foaf:*' => TRUE),
+   *           'datatype' => array('xsd:*' => TRUE),
+   *           'rel' => array('sioc:*' => TRUE),
+   *         ),
+   *         // Forbid the 'style' and 'on*' ('onClick' etc.) attributes on any
+   *         // tag. Only when *all* tags have the value FALSE, blacklisting
+   *         // will occur.
+   *         '*' => array(
+   *           'style' => FALSE,
+   *           'on*' => FALSE,
+   *         ),
+   *       )
+   *     )
+   *
+   *   A simpler example, for a very coarse filter:
+   *     array(
+   *       'forbiddenTags' => array('iframe', 'script')
+   *     )
+   *
+   *   The simplest example possible: a filter that doesn't allow any HTML:
+   *     array(
+   *       'allowedTags' => array()
+   *     )
+   *
+   * @see filter_get_html_restrictions_by_format()
+   */
+  public function getHTMLRestrictions();
+
+  /**
    * Generates a filter's tip.
    *
    * A filter's tips should be informative and to the point. Short tips are
diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php
index 3918ea9..e0649a3 100644
--- a/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php
+++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php
@@ -2,17 +2,19 @@
 
 /**
  * @file
- * Definition of Drupal\filter\Tests\FilterAPITest.
+ * Contains \Drupal\filter\Tests\FilterAPITest.
  */
 
 namespace Drupal\filter\Tests;
 
-use Drupal\simpletest\WebTestBase;
+use Drupal\simpletest\DrupalUnitTestBase;
 
 /**
  * Tests the behavior of Filter's API.
  */
-class FilterAPITest extends WebTestBase {
+class FilterAPITest extends DrupalUnitTestBase {
+
+  public static $modules = array('system', 'filter', 'filter_test');
 
   public static function getInfo() {
     return array(
@@ -25,6 +27,8 @@ public static function getInfo() {
   function setUp() {
     parent::setUp();
 
+    $this->installConfig(array('system'));
+
     // Create Filtered HTML format.
     $filtered_html_format = entity_create('filter_format', array(
       'format' => 'filtered_html',
@@ -38,6 +42,9 @@ function setUp() {
         // Note that the filter_html filter is of the type FILTER_TYPE_HTML_RESTRICTOR.
         'filter_html' => array(
           'status' => 1,
+          'settings' => array(
+            'allowed_html' => '<p> <br> <strong> <a>',
+          ),
         ),
       )
     ));
@@ -48,12 +55,7 @@ function setUp() {
       'format' => 'full_html',
       'name' => 'Full HTML',
       'weight' => 1,
-      'filters' => array(
-        'filter_htmlcorrector' => array(
-          'weight' => 10,
-          'status' => 1,
-        ),
-      ),
+      'filters' => array(),
     ));
     $full_html_format->save();
   }
@@ -88,11 +90,18 @@ function testCheckMarkup() {
   }
 
   /**
-   * Tests the function filter_get_filter_types_by_format().
+   * Tests the following functions for a variety of formats:
+   *   - filter_get_html_restrictions_by_format()
+   *   - filter_get_filter_types_by_format()
    */
   function testFilterFormatAPI() {
     // Test on filtered_html.
     $this->assertEqual(
+      filter_get_html_restrictions_by_format('filtered_html'),
+      array('allowedTags' => array('p' => TRUE, 'br' => TRUE, 'strong' => TRUE, 'a' => TRUE, '*' => array('style' => FALSE, 'on*' => FALSE))),
+      'filter_get_html_restrictions_by_format() works as expected for the filtered_html format.'
+    );
+    $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.'
@@ -100,10 +109,78 @@ function testFilterFormatAPI() {
 
     // Test on full_html.
     $this->assertEqual(
+      filter_get_html_restrictions_by_format('full_html'),
+      FALSE, // Every tag is allowed.
+      'filter_get_html_restrictions_by_format() works as expected for the full_html format.'
+    );
+    $this->assertEqual(
       filter_get_filter_types_by_format('full_html'),
-      array(FILTER_TYPE_HTML_RESTRICTOR),
+      array(),
       'filter_get_filter_types_by_format() works as expected for the full_html format.'
     );
+
+    // Test on stupid_filtered_html, where nothing is disallowed.
+    $stupid_filtered_html_format = entity_create('filter_format', array(
+      'format' => 'stupid_filtered_html',
+      'name' => 'Stupid Filtered HTML',
+      'filters' => array(
+        'filter_html' => array(
+          'status' => 1,
+          'settings' => array(
+            'allowed_html' => '', // Nothing is allowed.
+          ),
+        ),
+      ),
+    ));
+    $stupid_filtered_html_format->save();
+    $this->assertEqual(
+      filter_get_html_restrictions_by_format('stupid_filtered_html'),
+      array('allowedTags' => array()), // No tag is allowed.
+      'filter_get_html_restrictions_by_format() works as expected for the stupid_filtered_html format.'
+    );
+    $this->assertEqual(
+      filter_get_filter_types_by_format('stupid_filtered_html'),
+      array(FILTER_TYPE_HTML_RESTRICTOR),
+      'filter_get_filter_types_by_format() works as expected for the stupid_filtered_html format.'
+    );
+
+    // Test on very_restricted_html, where there's two different filters of the
+    // FILTER_TYPE_HTML_RESTRICTOR type, each restricting in different ways.
+    $very_restricted_html = entity_create('filter_format', array(
+      'format' => 'very_restricted_html',
+      'name' => 'Very Restricted HTML',
+      'filters' => array(
+        'filter_html' => array(
+          'status' => 1,
+          'settings' => array(
+            'allowed_html' => 'p br a strong',
+          ),
+        ),
+        'filter_test_restrict_tags_and_attributes' => array(
+          'status' => 1,
+          'settings' => array(
+            'restrictions' => array(
+              'allowedTags' => array(
+                'p' => TRUE,
+                'br' => FALSE,
+                'a' => array('href' => TRUE),
+              ),
+            )
+          ),
+        ),
+      )
+    ));
+    $very_restricted_html->save();
+    $this->assertEqual(
+      filter_get_html_restrictions_by_format('very_restricted_html'),
+      array('allowedTags' => array('p' => TRUE, 'br' => FALSE, 'a' => array('href' => TRUE), '*' => array('style' => FALSE, 'on*' => FALSE))),
+      'filter_get_html_restrictions_by_format() works as expected for the very_restricted_html format.'
+    );
+    $this->assertEqual(
+      filter_get_filter_types_by_format('very_restricted_html'),
+      array(FILTER_TYPE_HTML_RESTRICTOR),
+      'filter_get_filter_types_by_format() works as expected for the very_restricted_html format.'
+    );
   }
 
 }
diff --git a/core/modules/filter/tests/filter_test/lib/Drupal/filter_test/Plugin/Filter/FilterTestRestrictTagsAndAttributes.php b/core/modules/filter/tests/filter_test/lib/Drupal/filter_test/Plugin/Filter/FilterTestRestrictTagsAndAttributes.php
new file mode 100644
index 0000000..5576406
--- /dev/null
+++ b/core/modules/filter/tests/filter_test/lib/Drupal/filter_test/Plugin/Filter/FilterTestRestrictTagsAndAttributes.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\filter_test\Plugin\Filter\FilterTestRestrictTagsAndAttributes.
+ */
+
+namespace Drupal\filter_test\Plugin\Filter;
+
+use Drupal\filter\Annotation\Filter;
+use Drupal\Core\Annotation\Translation;
+use Drupal\filter\Plugin\FilterBase;
+
+/**
+ * Provides a test filter to restirct HTML tags and attributes.
+ *
+ * @Filter(
+ *   id = "filter_test_restrict_tags_and_attributes",
+ *   module = "filter_test",
+ *   title = @Translation("Tag & attribute restricting filter"),
+ *   description = @Translation("Replaces all content with filter and text format information."),
+ *   type = FILTER_TYPE_HTML_RESTRICTOR
+ * )
+ */
+class FilterTestRestrictTagsAndAttributes extends FilterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process($text, $langcode, $cache, $cache_id) {
+    return $text;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getHTMLRestrictions() {
+    return $this->settings['restrictions'];
+  }
+
+}
