Seditio Source
Root |
./othercms/dotclear-2.22/inc/libs/clearbricks/html.filter/class.html.filter.php
<?php
/**
 * @class htmlFilter
 * @brief HTML code filter
 *
 * This class removes all unwanted tags and attributes from an HTML string.
 *
 * This was inspired by Ulf Harnhammar's Kses (http://sourceforge.net/projects/kses)
 *
 * @package Clearbricks
 * @subpackage HTML
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright GPL-2.0-only
 */
class htmlFilter
{
    private
$parser;
    public
$content;

    private
$tag;

   
/**
     * Constructor
     *
     * Creates a new instance of the class.
     */
   
public function __construct(bool $keep_aria = false, bool $keep_data = false, bool $keep_js = false)
    {
       
$this->parser = xml_parser_create('UTF-8');
       
xml_set_object($this->parser, $this);
       
xml_set_element_handler($this->parser, [$this, 'tag_open'], [$this, 'tag_close']);
       
xml_set_character_data_handler($this->parser, [$this, 'cdata']);
       
xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, 0);

       
$this->removeTags(
           
'applet',
           
'base',
           
'basefont',
           
'body',
           
'center',
           
'dir',
           
'font',
           
'frame',
           
'frameset',
           
'head',
           
'html',
           
'isindex',
           
'link',
           
'menu',
           
'menuitem',
           
'meta',
           
'noframes',
           
'script',
           
'noscript',
           
'style'
       
);

       
// Remove aria-* and data-* attributes if necessary (tidy extension does it, not ready for HTML5)
       
if (!$keep_aria) {
           
$this->removePatternAttributes('^aria-[\-\w]+$');
           
$this->removeAttributes('role');
        }
        if (!
$keep_data) {
           
$this->removePatternAttributes('^data-[\-\w].*$');
        }

        if (!
$keep_js) {
           
// Remove events attributes
           
$this->removeArrayAttributes($this->event_attrs);
           
// Remove inline JS in URI
           
$this->removeHosts('javascript');
        }
    }

   
/**
     * Append hosts
     *
     * Appends hosts to remove from URI. Each method argument is a host. Example:
     *
     * <code>
     * <?php
     * $filter = new htmlFilter();
     * $filter->removeHosts('javascript');
     * ?>
     * </code>
     */
   
public function removeHosts()
    {
        foreach (
$this->argsArray(func_get_args()) as $host) {
           
$this->removed_hosts[] = $host;
        }
    }

   
/**
     * Append tags
     *
     * Appends tags to remove. Each method argument is a tag. Example:
     *
     * <code>
     * <?php
     * $filter = new htmlFilter();
     * $filter->removeTags('frame','script');
     * ?>
     * </code>
     */
   
public function removeTags()
    {
        foreach (
$this->argsArray(func_get_args()) as $tag) {
           
$this->removed_tags[] = $tag;
        }
    }

   
/**
     * Append attributes
     *
     * Appends attributes to remove. Each method argument is an attribute. Example:
     *
     * <code>
     * <?php
     * $filter = new htmlFilter();
     * $filter->removeAttributes('onclick','onunload');
     * ?>
     * </code>
     */
   
public function removeAttributes()
    {
        foreach (
$this->argsArray(func_get_args()) as $a) {
           
$this->removed_attrs[] = $a;
        }
    }

   
/**
     * Append array of attributes
     *
     * Appends attributes to remove. Example:
     *
     * <code>
     * <?php
     * $filter = new htmlFilter();
     * $filter->removeAttributes(['onload','onerror']);
     * ?>
     * </code>
     */
   
public function removeArrayAttributes($attrs)
    {
        foreach (
$attrs as $a) {
           
$this->removed_attrs[] = $a;
        }
    }

   
/**
     * Append attribute patterns
     *
     * Appends attribute patterns to remove. Each method argument is an attribute pattern. Example:
     *
     * <code>
     * <?php
     * $filter = new htmlFilter();
     * $filter->removeAttributes('data-.*');
     * ?>
     * </code>
     */
   
public function removePatternAttributes()
    {
        foreach (
$this->argsArray(func_get_args()) as $a) {
           
$this->removed_pattern_attrs[] = $a;
        }
    }

   
/**
     * Append attributes for tags
     *
     * Appends attributes to remove from specific tags. Each method argument is
     * an array of tags with attributes. Example:
     *
     * <code>
     * <?php
     * $filter = new htmlFilter();
     * $filter->removeTagAttributes(['a' => ['src','title']]);
     * ?>
     * </code>
     */
   
public function removeTagAttributes($tag)
    {
       
$args = $this->argsArray(func_get_args());
       
array_shift($args);

        foreach (
$args as $a) {
           
$this->removed_tag_attrs[$tag][] = $a;
        }
    }

   
/**
     * Known tags
     *
     * Creates a list of known tags.
     *
     * @param array        $t        Tags array
     */
   
public function setTags(array $t)
    {
        if (
is_array($t)) {
           
$this->tags = $t;
        }
    }

   
/**
     * Apply filter
     *
     * This method applies filter on given <var>$str</var> string. It will first
     * try to use tidy extension if exists and then apply the filter.
     *
     * @param string    $str        String to filter
     * @param boolean   $tidy       Use tidy extension if present
     *
     * @return string               Filtered string
     */
   
public function apply(string $str, bool $tidy = true)
    {
        if (
$tidy && extension_loaded('tidy') && class_exists('tidy')) {
           
$config = [
               
'doctype'                     => 'strict',
               
'drop-proprietary-attributes' => true,
               
'escape-cdata'                => true,
               
'indent'                      => false,
               
'join-classes'                => false,
               
'join-styles'                 => true,
               
'lower-literals'              => true,
               
'output-xhtml'                => true,
               
'show-body-only'              => true,
               
'wrap'                        => 80,
            ];

           
$str = '<p>tt</p>' . $str; // Fixes a big issue

           
$tidy = new tidy();
           
$tidy->parseString($str, $config, 'utf8');
           
$tidy->cleanRepair();

           
/* @phpstan-ignore-next-line */
           
$str = (string) $tidy;

           
$str = preg_replace('#^<p>tt</p>\s?#', '', $str);
        } else {
           
$str = $this->miniTidy($str);
        }

       
# Removing open comments, open CDATA and processing instructions
       
$str = preg_replace('%<!--.*?-->%msu', '', $str);
       
$str = str_replace('<!--', '', $str);
       
$str = preg_replace('%<!\[CDATA\[.*?\]\]>%msu', '', $str);
       
$str = str_replace('<![CDATA[', '', $str);

       
# Transform processing instructions
       
$str = str_replace('<?', '&gt;?', $str);
       
$str = str_replace('?>', '?&lt;', $str);

       
$str = html::decodeEntities($str, true);

       
$this->content = '';
       
xml_parse($this->parser, '<all>' . $str . '</all>');

        return
$this->content;
    }

    private function
miniTidy(string $str)
    {
       
$str = preg_replace_callback('%(<(?!(\s*?/|!)).*?>)%msu', [$this, 'miniTidyFixTag'], $str);

        return
$str;
    }

    private function
miniTidyFixTag(array $m)
    {
       
# Non quoted attributes
       
return preg_replace_callback('%(=")(.*?)(")%msu', [$this, 'miniTidyFixAttr'], $m[1]);
    }

    private function
miniTidyFixAttr(array $m)
    {
       
# Escape entities in attributes value
       
return $m[1] . html::escapeHTML(html::decodeEntities($m[2])) . $m[3];
    }

    private function
argsArray($args)
    {
       
$A = [];
        foreach (
$args as $v) {
            if (
is_array($v)) {
               
$A = array_merge($A, $v);
            } else {
               
$A[] = (string) $v;
            }
        }

        return
array_unique($A);
    }

    private function
tag_open($parser, $tag, $attrs)
    {
       
$this->tag = strtolower($tag);

        if (
$this->tag == 'all') {
            return;
        }

        if (
$this->allowedTag($this->tag)) {
           
$this->content .= '<' . $tag . $this->getAttrs($tag, $attrs);

            if (
in_array($this->tag, $this->single_tags)) {
               
$this->content .= ' />';
            } else {
               
$this->content .= '>';
            }
        }
    }

    private function
tag_close($parser, $tag)
    {
        if (!
in_array($tag, $this->single_tags) && $this->allowedTag($tag)) {
           
$this->content .= '</' . $tag . '>';
        }
    }

    private function
cdata($parser, $cdata)
    {
       
$this->content .= html::escapeHTML($cdata);
    }

    private function
getAttrs($tag, $attrs)
    {
       
$res = '';
        foreach (
$attrs as $n => $v) {
            if (
$this->allowedAttr($tag, $n)) {
               
$res .= $this->getAttr($n, $v);
            }
        }

        return
$res;
    }

    private function
getAttr($attr, $value)
    {
       
$value = preg_replace('/\xad+/', '', $value);

        if (
in_array($attr, $this->uri_attrs)) {
           
$value = $this->getURI($value);
        }

        return
' ' . $attr . '="' . html::escapeHTML($value) . '"';
    }

    private function
getURI($uri)
    {
       
// Trim URI
       
$uri = trim((string) $uri);
       
// Remove escaped Unicode characters
       
$uri = preg_replace('/\\\u[a-fA-F0-9]{4}/', '', $uri);
       
// Sanitize and parse URL
       
$uri = filter_var($uri, FILTER_SANITIZE_URL);
       
$u   = @parse_url($uri);

        if (
is_array($u) && (empty($u['scheme']) || in_array($u['scheme'], $this->allowed_schemes))) {
            if (empty(
$u['host']) || (!in_array($u['host'], $this->removed_hosts))) {
                return
$uri;
            }
        }

        return
'#';
    }

    private function
allowedTag($tag)
    {
        return
        !
in_array($tag, $this->removed_tags)
        && isset(
$this->tags[$tag]);
    }

    private function
allowedAttr($tag, $attr)
    {
        if (
in_array($attr, $this->removed_attrs)) {
            return
false;
        }

        if (isset(
$this->removed_tag_attrs[$tag]) && in_array($attr, $this->removed_tag_attrs[$tag])) {
            return
false;
        }

        if (!isset(
$this->tags[$tag]) || (!in_array($attr, $this->tags[$tag]) && // Not in tag allowed attributes
               
!in_array($attr, $this->gen_attrs) && // Not in allowed generic attributes
               
!in_array($attr, $this->event_attrs) && // Not in allowed event attributes
               
!$this->allowedPatternAttr($attr))) {     // Not in allowed grep attributes
           
return false;
        }

        return
true;
    }

    private function
allowedPatternAttr($attr)
    {
        foreach (
$this->removed_pattern_attrs as $pattern) {
            if (
preg_match('/' . $pattern . '/u', $attr)) {
                return
false;
            }
        }
        foreach (
$this->grep_attrs as $pattern) {
            if (
preg_match('/' . $pattern . '/u', $attr)) {
                return
true;
            }
        }

        return
false;
    }

   
/* Tags and attributes definitions
     * Source: https://developer.mozilla.org/fr/docs/Web/HTML/
    ------------------------------------------------------- */
   
private $removed_tags          = [];
    private
$removed_attrs         = [];
    private
$removed_pattern_attrs = [];
    private
$removed_tag_attrs     = [];
    private
$removed_hosts         = [];

    private
$allowed_schemes = [
       
'data',
       
'http',
       
'https',
       
'ftp',
       
'mailto',
       
'news',
    ];

   
// List of attributes which allow URL value
   
private $uri_attrs = [
       
'action',
       
'background',
       
'cite',
       
'classid',
       
'code',
       
'codebase',
       
'data',
       
'download',
       
'formaction',
       
'href',
       
'longdesc',
       
'profile',
       
'src',
       
'usemap',
    ];

   
// List of generic attributes
   
private $gen_attrs = [
       
'accesskey',
       
'class',
       
'contenteditable',
       
'contextmenu',
       
'dir',
       
'draggable',
       
'dropzone',
       
'hidden',
       
'id',
       
'itemid',
       
'itemprop',
       
'itemref',
       
'itemscope',
       
'itemtype',
       
'lang',
       
'role',
       
'slot',
       
'spellcheck',
       
'style',
       
'tabindex',
       
'title',
       
'translate',
       
'xml:base',
       
'xml:lang', ];

   
// List of events attributes
   
private $event_attrs = [
       
'onabort',
       
'onafterprint',
       
'onautocomplete',
       
'onautocompleteerror',
       
'onbeforeprint',
       
'onbeforeunload',
       
'onblur',
       
'oncancel',
       
'oncanplay',
       
'oncanplaythrough',
       
'onchange',
       
'onclick',
       
'onclose',
       
'oncontextmenu',
       
'oncuechange',
       
'ondblclick',
       
'ondrag',
       
'ondragend',
       
'ondragenter',
       
'ondragexit',
       
'ondragleave',
       
'ondragover',
       
'ondragstart',
       
'ondrop',
       
'ondurationchange',
       
'onemptied',
       
'onended',
       
'onerror',
       
'onfocus',
       
'onhashchange',
       
'oninput',
       
'oninvalid',
       
'onkeydown',
       
'onkeypress',
       
'onkeyup',
       
'onlanguagechange',
       
'onload',
       
'onloadeddata',
       
'onloadedmetadata',
       
'onloadstart',
       
'onmessage',
       
'onmousedown',
       
'onmouseenter',
       
'onmouseleave',
       
'onmousemove',
       
'onmouseout',
       
'onmouseover',
       
'onmouseup',
       
'onmousewheel',
       
'onoffline',
       
'ononline',
       
'onpause',
       
'onplay',
       
'onplaying',
       
'onpopstate',
       
'onprogress',
       
'onratechange',
       
'onredo',
       
'onreset',
       
'onresize',
       
'onscroll',
       
'onseeked',
       
'onseeking',
       
'onselect',
       
'onshow',
       
'onsort',
       
'onstalled',
       
'onstorage',
       
'onsubmit',
       
'onsuspend',
       
'ontimeupdate',
       
'ontoggle',
       
'onundo',
       
'onunload',
       
'onvolumechange',
       
'onwaiting',
    ];

   
// List of pattern'ed attributes
   
private $grep_attrs = [
       
'^aria-[\-\w]+$',
       
'^data-[\-\w].*$',
    ];

   
// List of single tags
   
private $single_tags = [
       
'area',
       
'base',
       
'basefont',
       
'br',
       
'col',
       
'embed',
       
'frame',
       
'hr',
       
'img',
       
'input',
       
'isindex',
       
'keygen',
       
'link',
       
'meta',
       
'param',
       
'source',
       
'track',
       
'wbr',
    ];

    private
$tags = [
       
// A
       
'a' => ['charset', 'coords', 'download', 'href', 'hreflang', 'name', 'ping', 'referrerpolicy',
           
'rel', 'rev', 'shape', 'target', 'type', ],
       
'abbr'    => [],
       
'acronym' => [],
       
'address' => [],
       
'applet'  => ['align', 'alt', 'archive', 'code', 'codebase', 'datafld', 'datasrc', 'height', 'hspace',
           
'mayscript', 'name', 'object', 'vspace', 'width', ],
       
'area' => ['alt', 'coords', 'download', 'href', 'name', 'media', 'nohref', 'referrerpolicy', 'rel',
           
'shape', 'target', 'type', ],
       
'article' => [],
       
'aside'   => [],
       
'audio'   => ['autoplay', 'buffered', 'controls', 'loop', 'muted', 'played', 'preload', 'src', 'volume'],
       
// B
       
'b'          => [],
       
'base'       => ['href', 'target'],
       
'basefont'   => ['color', 'face', 'size'],
       
'bdi'        => [],
       
'bdo'        => [],
       
'big'        => [],
       
'blockquote' => ['cite'],
       
'body'       => ['alink', 'background', 'bgcolor', 'bottommargin', 'leftmargin', 'link', 'text', 'rightmargin',
           
'text', 'topmargin', 'vlink', ],
       
'br'     => ['clear'],
       
'button' => ['autofocus', 'autocomplete', 'disabled', 'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'name', 'type', 'value'],
       
// C
       
'canvas'   => ['height', 'width'],
       
'caption'  => ['align'],
       
'center'   => [],
       
'cite'     => [],
       
'code'     => [],
       
'col'      => ['align', 'bgcolor', 'char', 'charoff', 'span', 'valign', 'width'],
       
'colgroup' => ['align', 'bgcolor', 'char', 'charoff', 'span', 'valign', 'width'],
       
// D
       
'data'     => ['value'],
       
'datalist' => [],
       
'dd'       => ['nowrap'],
       
'del'      => ['cite', 'datetime'],
       
'details'  => ['open'],
       
'dfn'      => [],
       
'dialog'   => ['open'],
       
'dir'      => ['compact'],
       
'div'      => ['align'],
       
'dl'       => [],
       
'dt'       => [],
       
// E
       
'em'    => [],
       
'embed' => ['height', 'src', 'type', 'width'],
       
// F
       
'fieldset'   => ['disabled', 'form', 'name'],
       
'figcaption' => [],
       
'figure'     => [],
       
'font'       => ['color', 'face', 'size'],
       
'footer'     => [],
       
'form'       => ['accept', 'accept-charset', 'action', 'autocapitalize', 'autocomplete', 'enctype', 'method',
           
'name', 'novalidate', 'target', ],
       
'frame'    => ['frameborder', 'marginheight', 'marginwidth', 'name', 'noresize', 'scrolling', 'src'],
       
'frameset' => ['cols', 'rows'],
       
// G
        // H
       
'h1'   => ['align'],
       
'h2'   => ['align'],
       
'h3'   => ['align'],
       
'h4'   => ['align'],
       
'h5'   => ['align'],
       
'h6'   => ['align'],
       
'head' => ['profile'],
       
'hr'   => ['align', 'color', 'noshade', 'size', 'width'],
       
'html' => ['manifest', 'version', 'xmlns'],
       
// I
       
'i'      => [],
       
'iframe' => ['align', 'allowfullscreen', 'allowpaymentrequest', 'frameborder', 'height', 'longdesc',
           
'marginheight', 'marginwidth', 'name', 'referrerpolicy', 'sandbox', 'scrolling', 'src', 'srcdoc', 'width', ],
       
'img' => ['align', 'alt', 'border', 'crossorigin', 'decoding', 'height', 'hspace', 'ismap', 'longdesc',
           
'name', 'referrerpolicy', 'sizes', 'src', 'srcset', 'usemap', 'vspace', 'width', ],
       
'input' => ['accept', 'alt', 'autocomplete', 'autofocus', 'capture', 'checked', 'disabled', 'form',
           
'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'height', 'inputmode', 'ismap',
           
'list', 'max', 'maxlength', 'min', 'minlength', 'multiple', 'name', 'pattern', 'placeholder', 'readonly',
           
'required', 'selectionDirection', 'selectionEnd', 'selectionStart', 'size', 'spellcheck', 'src', 'step', 'type',
           
'usemap', 'value', 'width', ],
       
'ins'     => ['cite', 'datetime'],
       
'isindex' => ['action', 'prompt'],
       
// J
        // K
       
'kbd'    => [],
       
'keygen' => ['autofocus', 'challenge', 'disabled', 'form', 'keytype', 'name'],
       
// L
       
'label'  => ['for', 'form'],
       
'legend' => [],
       
'li'     => ['type', 'value'],
       
'link'   => ['as', 'crossorigin', 'charset', 'disabled', 'href', 'hreflang', 'integrity', 'media', 'methods', 'prefetch', 'referrerpolicy', 'rel', 'rev', 'sizes', 'target', 'type'],
       
// M
       
'main'     => [],
       
'map'      => ['name'],
       
'mark'     => [],
       
'menu'     => ['label', 'type'],
       
'menuitem' => ['checked', 'command', 'default', 'disabled', 'icon', 'label', 'radiogroup', 'type'],
       
'meta'     => ['charset', 'content', 'http-equiv', 'name', 'scheme'],
       
'meter'    => ['form', 'high', 'low', 'max', 'min', 'optimum', 'value'],
       
// N
       
'nav'      => [],
       
'noframes' => [],
       
'noscript' => [],
       
// O
       
'object' => ['archive', 'border', 'classid', 'codebase', 'codetype', 'data', 'declare', 'form', 'height',
           
'hspace', 'name', 'standby', 'type', 'typemustmatch', 'usemap', 'width', ],
       
'ol'       => ['compact', 'reversed', 'start', 'type'],
       
'optgroup' => ['disabled', 'label'],
       
'option'   => ['disabled', 'label', 'selected', 'value'],
       
'output'   => ['for', 'form', 'name'],
       
// P
       
'p'        => ['align'],
       
'param'    => ['name', 'type', 'value', 'valuetype'],
       
'picture'  => [],
       
'pre'      => ['cols', 'width', 'wrap'],
       
'progress' => ['max', 'value'],
       
// Q
       
'q' => ['cite'],
       
// R
       
'rp'   => [],
       
'rt'   => [],
       
'rtc'  => [],
       
'ruby' => [],
       
// S
       
's'      => [],
       
'samp'   => [],
       
'script' => ['async', 'charset', 'crossorigin', 'defer', 'integrity', 'language', 'nomodule', 'nonce',
           
'src', 'type', ],
       
'section' => [],
       
'select'  => ['autofocus', 'disabled', 'form', 'multiple', 'name', 'required', 'size'],
       
'small'   => [],
       
'source'  => ['media', 'sizes', 'src', 'srcset', 'type'],
       
'span'    => [],
       
'strike'  => [],
       
'strong'  => [],
       
'style'   => ['media', 'nonce', 'scoped', 'type'],
       
'sub'     => [],
       
'summary' => [],
       
'sup'     => [],
       
// T
       
'table' => ['align', 'bgcolor', 'border', 'cellpadding', 'cellspacing', 'frame', 'rules', 'summary', 'width'],
       
'tbody' => ['align', 'bgcolor', 'char', 'charoff', 'valign'],
       
'td'    => ['abbr', 'align', 'axis', 'bgcolor', 'char', 'charoff', 'colspan', 'headers', 'nowrap',
           
'rowspan', 'scope', 'valign', 'width', ],
       
'template' => [],
       
'textarea' => ['autocomplete', 'autofocus', 'cols', 'disabled', 'form', 'maxlength', 'minlength', 'name',
           
'placeholder', 'readonly', 'rows', 'spellcheck', 'wrap', ],
       
'tfoot' => ['align', 'bgcolor', 'char', 'charoff', 'valign'],
       
'th'    => ['abbr', 'align', 'axis', 'bgcolor', 'char', 'charoff', 'colspan', 'headers', 'nowrap',
           
'rowspan', 'scope', 'valign', 'width', ],
       
'thead' => ['align', 'bgcolor', 'char', 'charoff', 'valign'],
       
'time'  => ['datetime'],
       
'title' => [],
       
'tr'    => ['align', 'bgcolor', 'char', 'charoff', 'valign'],
       
'track' => ['default', 'kind', 'label', 'src', 'srclang'],
       
'tt'    => [],
       
// U
       
'u'  => [],
       
'ul' => ['compact', 'type'],
       
// V
       
'var'   => [],
       
'video' => ['autoplay', 'buffered', 'controls', 'crossorigin', 'height', 'loop', 'muted', 'played',
           
'playsinline', 'preload', 'poster', 'src', 'width', ],
       
// W
       
'wbr' => [],
       
// X
        // Y
        // Z
   
];
}