Commit 31fe58d1 authored by Dries's avatar Dries

Issue #1936392 by Wim Leers: Configure CKEditor's 'Advanced Content Filter'...

Issue #1936392 by Wim Leers: Configure CKEditor's 'Advanced Content Filter' (ACF) to match Drupal's text filters settings.
parent 00cb147e
......@@ -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);
......@@ -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,149 @@ 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['allowed'] 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. The
// "style" and "class" attributes are handled separately by CKEditor:
// they are disallowed even if you specify it in the list of allowed
// attributes, unless you state specific values for them that are
// allowed. Or, in this case: any value for them is allowed.
elseif ($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['allowed']['*'])) {
$wildcard = $html_restrictions['allowed']['*'];
if (isset($wildcard['style'])) {
if (!is_array($wildcard['style'])) {
$setting[$tag]['styles'] = $wildcard['style'];
}
else {
$allowed_styles = $get_allowed_attribute_values($wildcard['style']);
if (isset($allowed_styles)) {
$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_classes = $get_allowed_attribute_values($wildcard['class']);
if (isset($allowed_classes)) {
$setting[$tag]['classes'] = $allowed_classes;
}
else {
unset($setting[$tag]['classes']);
}
}
}
}
}
// Tell CKEditor the tag is allowed, along with some tags.
elseif (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;
}
}
}
......@@ -186,6 +186,8 @@ public function getJSSettings(EditorEntity $editor) {
'drupalExternalPlugins' => array_map('file_create_url', $external_plugins),
);
ksort($settings);
return $settings;
}
......
......@@ -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' => '<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' => '',
......@@ -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,81 @@ 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'] .= '<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.');
$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(
'allowed' => 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,
'class' => array('is-a-hipster-llama' => TRUE, 'and-more' => TRUE),
'data-*' => TRUE,
),
'del' => FALSE,
)
),
),
));
$format->save();
$expected_config['allowedContent'] = array(
'p' => array(
'attributes' => TRUE,
'styles' => FALSE,
'classes' => 'is-a-hipster-llama,and-more',
),
'a' => array(
'attributes' => 'href,rel,class,target',
'classes' => 'external',
),
'span' => array(
'attributes' => 'class,property,rel',
),
'*' => array(
'attributes' => 'class,data-*',
'classes' => 'is-a-hipster-llama,and-more',
),
'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 +237,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.
......@@ -236,6 +308,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')),
......
......@@ -415,6 +415,154 @@ function filter_get_filter_types_by_format($format_id) {
return array_unique($filter_types);
}
/**
* 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['forbidden_tags'])) {
if (!isset($restrictions['forbidden_tags'])) {
$restrictions['forbidden_tags'] = $new_restrictions['forbidden_tags'];
}
else {
$restrictions['forbidden_tags'] = array_unique(array_merge($restrictions['forbidden_tags'], $new_restrictions['forbidden_tags']));
}
}
// Track the intersection of allowed (whitelisted) tags.
if (isset($restrictions['allowed'])) {
$intersection = $restrictions['allowed'];
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['allowed'])) {
// 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['allowed'][$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['allowed'] = $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['allowed']) && isset($restrictions['forbidden_tags'])) {
foreach ($restrictions['forbidden_tags'] as $tag) {
if (isset($restrictions['allowed'][$tag])) {
unset($restrictions['allowed'][$tag]);
}
}
unset($restrictions['forbidden_tags']);
}
// 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['allowed'])) {
if (count($restrictions['allowed']) === 1 && array_key_exists('*', $restrictions['allowed']) && !isset($restrictions['forbidden_tags'])) {
$restrictions['allowed'] = array();
}
}
return $restrictions;
}
}
/**
* Returns the ID of the fallback text format that all users have access to.
*
......
......@@ -65,6 +65,21 @@ public function process($text, $langcode, $cache, $cache_id) {
return _filter_html($text, $this);
}
/**
* {@inheritdoc}
*/
public function getHTMLRestrictions() {
$restrictions = array('allowed' => array());
$tags = preg_split('/\s+|<|>/', $this->settings['allowed_html'], -1, PREG_SPLIT_NO_EMPTY);
// List the allowed HTML tags.
foreach ($tags as $tag) {
$restrictions['allowed'][$tag] = TRUE;
}
// The 'style' and 'on*' ('onClick' etc.) attributes are always forbidden.
$restrictions['allowed']['*'] = array('style' => FALSE, 'on*' => FALSE);
return $restrictions;
}
/**
* {@inheritdoc}
*/
......
......@@ -31,6 +31,14 @@ public function process($text, $langcode, $cache, $cache_id) {
return _filter_html_escape($text);
}
/**
* {@inheritdoc}
*/
public function getHTMLRestrictions() {
// Nothing is allowed.
return array('allowed' => array());
}
/**
* {@inheritdoc}
*/
......
......@@ -147,6 +147,13 @@ public function prepare($text, $langcode, $cache, $cache_id) {
return $text;
}
/**
* {@inheritdoc}
*/
public function getHTMLRestrictions() {
return FALSE;
}
/**
* {@inheritdoc}
*/
......
......@@ -175,6 +175,100 @@ 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 *either* of the following keys:
* - 'allowed': (optional) the allowed tags as keys, and for each of those
* tags (keys) either of the following values:
* - TRUE to indicate any attribute is allowed
* - FALSE to indicate no attributes are allowed
* - an array to convey attribute restrictions: the keys must be
* attribute names (which may use a wildcard, e.g. "data-*"), the
* possible values are similar to the above:
* - TRUE to indicate any attribute value is allowed
* - FALSE to indicate the attribute is forbidden
* - an array to convey attribute value restrictions: the key must
* be attribute values (which may use a wildcard, e.g. "xsd:*"),
* the possible values are TRUE or FALSE: to mark the attribute
* value as allowed or forbidden, respectively
* - 'forbidden_tags': (optional) the forbidden tags
*
* There is one special case: the "wildcard tag", "*": any attribute
* restrictions on that pseudotag apply to all tags.
*
* If no restrictions apply, then FALSE must be returned.
*
* Here is a concrete example, for a very granular filter:
* @code
* array(
* 'allowed' => 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.
* '*' => array(
* 'style' => FALSE,
* 'on*' => FALSE,