View source
<?php
namespace Drupal\filter\Plugin\Filter;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\Html;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;
class FilterHtml extends FilterBase {
protected $restrictions;
public function settingsForm(array $form, FormStateInterface $form_state) {
$form['allowed_html'] = [
'#type' => 'textarea',
'#title' => $this
->t('Allowed HTML tags'),
'#default_value' => $this->settings['allowed_html'],
'#description' => $this
->t('A list of HTML tags that can be used. By default only the <em>lang</em> and <em>dir</em> attributes are allowed for all HTML tags. Each HTML tag may have attributes which are treated as allowed attribute names for that HTML tag. Each attribute may allow all values, or only allow specific values. Attribute names or values may be written as a prefix and wildcard like <em>jump-*</em>. JavaScript event attributes, JavaScript URLs, and CSS are always stripped.'),
'#attached' => [
'library' => [
'filter/drupal.filter.filter_html.admin',
],
],
];
$form['filter_html_help'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Display basic HTML help in long filter tips'),
'#default_value' => $this->settings['filter_html_help'],
];
$form['filter_html_nofollow'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Add rel="nofollow" to all links'),
'#default_value' => $this->settings['filter_html_nofollow'],
];
return $form;
}
public function setConfiguration(array $configuration) {
if (isset($configuration['settings']['allowed_html'])) {
$configuration['settings']['allowed_html'] = preg_replace('/\\s+/', ' ', $configuration['settings']['allowed_html']);
}
parent::setConfiguration($configuration);
$this->restrictions = NULL;
}
public function process($text, $langcode) {
$restrictions = $this
->getHtmlRestrictions();
unset($restrictions['allowed']['*']);
$text = Xss::filter($text, array_keys($restrictions['allowed']));
return new FilterProcessResult($this
->filterAttributes($text));
}
public function filterAttributes($text) {
$restrictions = $this
->getHTMLRestrictions();
$global_allowed_attributes = array_filter($restrictions['allowed']['*']);
unset($restrictions['allowed']['*']);
$html_dom = Html::load($text);
$xpath = new \DOMXPath($html_dom);
foreach ($restrictions['allowed'] as $allowed_tag => $tag_attributes) {
if ($tag_attributes === FALSE) {
$tag_attributes = [];
}
$allowed_attributes = [
'exact' => [],
'prefix' => [],
];
foreach ($global_allowed_attributes + $tag_attributes as $name => $values) {
if (substr($name, -1) === '*' && $name[0] !== '*') {
$allowed_attributes['prefix'][str_replace('*', '', $name)] = $this
->prepareAttributeValues($values);
}
else {
$allowed_attributes['exact'][$name] = $this
->prepareAttributeValues($values);
}
}
krsort($allowed_attributes['prefix']);
foreach ($xpath
->query('//' . $allowed_tag . '[@*]') as $element) {
$this
->filterElementAttributes($element, $allowed_attributes);
}
}
if ($this->settings['filter_html_nofollow']) {
$links = $html_dom
->getElementsByTagName('a');
foreach ($links as $link) {
$link
->setAttribute('rel', 'nofollow');
}
}
$text = Html::serialize($html_dom);
return trim($text);
}
protected function filterElementAttributes(\DOMElement $element, array $allowed_attributes) {
$modified_attributes = [];
foreach ($element->attributes as $name => $attribute) {
$allowed_value = $this
->findAllowedValue($allowed_attributes, $name);
if (empty($allowed_value)) {
$modified_attributes[$name] = FALSE;
}
elseif ($allowed_value !== TRUE) {
$attribute_values = preg_split('/\\s+/', $attribute->value, -1, PREG_SPLIT_NO_EMPTY);
$modified_attributes[$name] = [];
foreach ($attribute_values as $value) {
if ($this
->findAllowedValue($allowed_value, $value)) {
$modified_attributes[$name][] = $value;
}
}
}
}
foreach ($modified_attributes as $name => $values) {
if ($values) {
$element
->setAttribute($name, implode(' ', $values));
}
else {
$element
->removeAttribute($name);
}
}
}
protected function findAllowedValue(array $allowed, $name) {
if (isset($allowed['exact'][$name])) {
return $allowed['exact'][$name];
}
foreach ($allowed['prefix'] as $prefix => $value) {
if (strpos($name, $prefix) === 0) {
return $value;
}
}
return FALSE;
}
protected function prepareAttributeValues($attribute_values) {
if ($attribute_values === TRUE || $attribute_values === FALSE) {
return $attribute_values;
}
$result = [
'exact' => [],
'prefix' => [],
];
foreach ($attribute_values as $name => $allowed) {
if (substr($name, -1) === '*' && $name[0] !== '*') {
$result['prefix'][str_replace('*', '', $name)] = $allowed;
}
else {
$result['exact'][$name] = $allowed;
}
}
krsort($result['prefix']);
return $result;
}
public function getHTMLRestrictions() {
if ($this->restrictions) {
return $this->restrictions;
}
$restrictions = [
'allowed' => [],
];
$html = str_replace('>', ' />', $this->settings['allowed_html']);
$star_protector = '__zqh6vxfbk3cg__';
$html = str_replace('*', $star_protector, $html);
$body_child_nodes = Html::load($html)
->getElementsByTagName('body')
->item(0)->childNodes;
foreach ($body_child_nodes as $node) {
if ($node->nodeType !== XML_ELEMENT_NODE) {
continue;
}
$tag = $node->tagName;
if ($node
->hasAttributes()) {
$restrictions['allowed'][$tag] = [];
foreach ($node->attributes as $name => $attribute) {
$name = str_replace($star_protector, '*', $name);
$allowed_attribute_values = preg_split('/\\s+/', str_replace($star_protector, '*', $attribute->value), -1, PREG_SPLIT_NO_EMPTY);
$allowed_attribute_values = array_filter($allowed_attribute_values, function ($value) use ($star_protector) {
return $value !== '*';
});
if (empty($allowed_attribute_values)) {
$restrictions['allowed'][$tag][$name] = TRUE;
}
else {
foreach ($allowed_attribute_values as $value) {
$restrictions['allowed'][$tag][$name][$value] = TRUE;
}
}
}
}
else {
$restrictions['allowed'][$tag] = FALSE;
}
}
$restrictions['allowed']['*'] = [
'style' => FALSE,
'on*' => FALSE,
'lang' => TRUE,
'dir' => [
'ltr' => TRUE,
'rtl' => TRUE,
],
];
$this->restrictions = $restrictions;
return $restrictions;
}
public function tips($long = FALSE) {
global $base_url;
if (!($allowed_html = $this->settings['allowed_html'])) {
return;
}
$output = $this
->t('Allowed HTML tags: @tags', [
'@tags' => $allowed_html,
]);
if (!$long) {
return $output;
}
$output = '<p>' . $output . '</p>';
if (!$this->settings['filter_html_help']) {
return $output;
}
$output .= '<p>' . $this
->t('This site allows HTML content. While learning all of HTML may feel intimidating, learning how to use a very small number of the most basic HTML "tags" is very easy. This table provides examples for each tag that is enabled on this site.') . '</p>';
$output .= '<p>' . $this
->t('For more information see the <a href=":html-specifications">HTML Living Standard</a> or use your favorite search engine to find other sites that explain HTML.', [
':html-specifications' => 'https://html.spec.whatwg.org/',
]) . '</p>';
$tips = [
'a' => [
$this
->t('Anchors are used to make links to other pages.'),
'<a href="' . $base_url . '">' . Html::escape(\Drupal::config('system.site')
->get('name')) . '</a>',
],
'br' => [
$this
->t('By default line break tags are automatically added, so use this tag to add additional ones. Use of this tag is different because it is not used with an open/close pair like all the others. Use the extra " /" inside the tag to maintain XHTML 1.0 compatibility'),
$this
->t('Text with <br />line break'),
],
'p' => [
$this
->t('By default paragraph tags are automatically added, so use this tag to add additional ones.'),
'<p>' . $this
->t('Paragraph one.') . '</p> <p>' . $this
->t('Paragraph two.') . '</p>',
],
'strong' => [
$this
->t('Strong', [], [
'context' => 'Font weight',
]),
'<strong>' . $this
->t('Strong', [], [
'context' => 'Font weight',
]) . '</strong>',
],
'em' => [
$this
->t('Emphasized'),
'<em>' . $this
->t('Emphasized') . '</em>',
],
'cite' => [
$this
->t('Cited'),
'<cite>' . $this
->t('Cited') . '</cite>',
],
'code' => [
$this
->t('Coded text used to show programming source code'),
'<code>' . $this
->t('Coded') . '</code>',
],
'b' => [
$this
->t('Bolded'),
'<b>' . $this
->t('Bolded') . '</b>',
],
'u' => [
$this
->t('Underlined'),
'<u>' . $this
->t('Underlined') . '</u>',
],
'i' => [
$this
->t('Italicized'),
'<i>' . $this
->t('Italicized') . '</i>',
],
'sup' => [
$this
->t('Superscripted'),
$this
->t('<sup>Super</sup>scripted'),
],
'sub' => [
$this
->t('Subscripted'),
$this
->t('<sub>Sub</sub>scripted'),
],
'pre' => [
$this
->t('Preformatted'),
'<pre>' . $this
->t('Preformatted') . '</pre>',
],
'abbr' => [
$this
->t('Abbreviation'),
$this
->t('<abbr title="Abbreviation">Abbrev.</abbr>'),
],
'acronym' => [
$this
->t('Acronym'),
$this
->t('<acronym title="Three-Letter Acronym">TLA</acronym>'),
],
'blockquote' => [
$this
->t('Block quoted'),
'<blockquote>' . $this
->t('Block quoted') . '</blockquote>',
],
'q' => [
$this
->t('Quoted inline'),
'<q>' . $this
->t('Quoted inline') . '</q>',
],
'table' => [
$this
->t('Table'),
'<table> <tr><th>' . $this
->t('Table header') . '</th></tr> <tr><td>' . $this
->t('Table cell') . '</td></tr> </table>',
],
'tr' => NULL,
'td' => NULL,
'th' => NULL,
'del' => [
$this
->t('Deleted'),
'<del>' . $this
->t('Deleted') . '</del>',
],
'ins' => [
$this
->t('Inserted'),
'<ins>' . $this
->t('Inserted') . '</ins>',
],
'ol' => [
$this
->t('Ordered list - use the <li> to begin each list item'),
'<ol> <li>' . $this
->t('First item') . '</li> <li>' . $this
->t('Second item') . '</li> </ol>',
],
'ul' => [
$this
->t('Unordered list - use the <li> to begin each list item'),
'<ul> <li>' . $this
->t('First item') . '</li> <li>' . $this
->t('Second item') . '</li> </ul>',
],
'li' => NULL,
'dl' => [
$this
->t('Definition lists are similar to other HTML lists. <dl> begins the definition list, <dt> begins the definition term and <dd> begins the definition description.'),
'<dl> <dt>' . $this
->t('First term') . '</dt> <dd>' . $this
->t('First definition') . '</dd> <dt>' . $this
->t('Second term') . '</dt> <dd>' . $this
->t('Second definition') . '</dd> </dl>',
],
'dt' => NULL,
'dd' => NULL,
'h1' => [
$this
->t('Heading'),
'<h1>' . $this
->t('Title') . '</h1>',
],
'h2' => [
$this
->t('Heading'),
'<h2>' . $this
->t('Subtitle') . '</h2>',
],
'h3' => [
$this
->t('Heading'),
'<h3>' . $this
->t('Subtitle three') . '</h3>',
],
'h4' => [
$this
->t('Heading'),
'<h4>' . $this
->t('Subtitle four') . '</h4>',
],
'h5' => [
$this
->t('Heading'),
'<h5>' . $this
->t('Subtitle five') . '</h5>',
],
'h6' => [
$this
->t('Heading'),
'<h6>' . $this
->t('Subtitle six') . '</h6>',
],
];
$header = [
$this
->t('Tag Description'),
$this
->t('You Type'),
$this
->t('You Get'),
];
preg_match_all('/<([a-z0-9]+)[^a-z0-9]/i', $allowed_html, $out);
foreach ($out[1] as $tag) {
if (!empty($tips[$tag])) {
$rows[] = [
[
'data' => $tips[$tag][0],
'class' => [
'description',
],
],
[
'data' => [
'#prefix' => '<code>',
'#plain_text' => $tips[$tag][1],
'#suffix' => '</code>',
],
'class' => [
'type',
],
],
[
'data' => [
'#markup' => $tips[$tag][1],
],
'class' => [
'get',
],
],
];
}
else {
$rows[] = [
[
'data' => $this
->t('No help provided for tag %tag.', [
'%tag' => $tag,
]),
'class' => [
'description',
],
'colspan' => 3,
],
];
}
}
$table = [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
];
$output .= \Drupal::service('renderer')
->render($table);
$output .= '<p>' . $this
->t('Most unusual characters can be directly entered without any problems.') . '</p>';
$output .= '<p>' . $this
->t('If you do encounter problems, try using HTML character entities. A common example looks like &amp; for an ampersand & character. For a full list of entities see HTML\'s <a href=":html-entities">entities</a> page. Some of the available characters include:', [
':html-entities' => 'http://www.w3.org/TR/html4/sgml/entities.html',
]) . '</p>';
$entities = [
[
$this
->t('Ampersand'),
'&',
],
[
$this
->t('Greater than'),
'>',
],
[
$this
->t('Less than'),
'<',
],
[
$this
->t('Quotation mark'),
'"',
],
];
$header = [
$this
->t('Character Description'),
$this
->t('You Type'),
$this
->t('You Get'),
];
unset($rows);
foreach ($entities as $entity) {
$rows[] = [
[
'data' => $entity[0],
'class' => [
'description',
],
],
[
'data' => [
'#prefix' => '<code>',
'#plain_text' => $entity[1],
'#suffix' => '</code>',
],
'class' => [
'type',
],
],
[
'data' => [
'#markup' => $entity[1],
],
'class' => [
'get',
],
],
];
}
$table = [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
];
$output .= \Drupal::service('renderer')
->render($table);
return $output;
}
}