* * @package External * @subpackage KSES */ /** * Specifies the default allowable HTML tags. * * Using `CUSTOM_TAGS` is not recommended and should be considered deprecated. The * {@see 'wp_kses_allowed_html'} filter is more powerful and supplies context. * * When using this constant, make sure to set all of these globals to arrays: * * - `$allowedposttags` * - `$allowedtags` * - `$allowedentitynames` * - `$allowedxmlentitynames` * * @see wp_kses_allowed_html() * @since 1.2.0 * * @var array[]|false Array of default allowable HTML tags, or false to use the defaults. */ if ( ! defined( 'CUSTOM_TAGS' ) ) { define( 'CUSTOM_TAGS', false ); } // Ensure that these variables are added to the global namespace // (e.g. if using namespaces / autoload in the current PHP environment). global $allowedposttags, $allowedtags, $allowedentitynames, $allowedxmlentitynames; /** * Indicates which implementation of {@see \wp_kses()} is running. * * Nominally `legacy` unless temporarily-switched for {@see \wp_sanitize_html_kses()}. * It’s safe to latch this into `legacy`. * * @global 'legacy'|'html-api' $wp_kses_operating_mode * * @since 7.2.0 */ global $wp_kses_operating_mode; $wp_kses_operating_mode = 'legacy'; if ( ! CUSTOM_TAGS ) { /** * KSES global for default allowable HTML tags. * * Can be overridden with the `CUSTOM_TAGS` constant. * * @var array[] $allowedposttags Array of default allowable HTML tags. * @since 2.0.0 */ $allowedposttags = array( 'address' => array(), 'a' => array( 'href' => true, 'rel' => true, 'rev' => true, 'name' => true, 'target' => true, 'download' => array( 'valueless' => 'y', ), ), 'abbr' => array(), 'acronym' => array(), 'area' => array( 'alt' => true, 'coords' => true, 'href' => true, 'nohref' => true, 'shape' => true, 'target' => true, ), 'article' => array( 'align' => true, ), 'aside' => array( 'align' => true, ), 'audio' => array( 'autoplay' => true, 'controls' => true, 'loop' => true, 'muted' => true, 'preload' => true, 'src' => true, ), 'b' => array(), 'bdo' => array(), 'big' => array(), 'blockquote' => array( 'cite' => true, ), 'br' => array(), 'button' => array( 'command' => true, 'commandfor' => true, 'disabled' => true, 'name' => true, 'type' => true, 'value' => true, 'popovertarget' => true, 'popovertargetaction' => true, 'aria-haspopup' => true, ), 'caption' => array( 'align' => true, ), 'cite' => array(), 'code' => array(), 'col' => array( 'align' => true, 'char' => true, 'charoff' => true, 'span' => true, 'valign' => true, 'width' => true, ), 'colgroup' => array( 'align' => true, 'char' => true, 'charoff' => true, 'span' => true, 'valign' => true, 'width' => true, ), 'data' => array( 'value' => true, ), 'del' => array( 'datetime' => true, ), 'dd' => array(), 'dfn' => array(), 'details' => array( 'align' => true, 'open' => true, 'name' => true, ), 'div' => array( 'align' => true, 'popover' => true, ), 'dialog' => array( 'closedby' => true, 'open' => true, 'popover' => true, 'autofocus' => true, ), 'dl' => array(), 'dt' => array(), 'em' => array(), 'fieldset' => array(), 'figure' => array( 'align' => true, ), 'figcaption' => array( 'align' => true, ), 'font' => array( 'color' => true, 'face' => true, 'size' => true, ), 'footer' => array( 'align' => true, ), 'h1' => array( 'align' => true, ), 'h2' => array( 'align' => true, ), 'h3' => array( 'align' => true, ), 'h4' => array( 'align' => true, ), 'h5' => array( 'align' => true, ), 'h6' => array( 'align' => true, ), 'header' => array( 'align' => true, ), 'hgroup' => array( 'align' => true, ), 'hr' => array( 'align' => true, 'noshade' => true, 'size' => true, 'width' => true, ), 'i' => array(), 'img' => array( 'alt' => true, 'align' => true, 'border' => true, 'height' => true, 'hspace' => true, 'loading' => true, 'longdesc' => true, 'vspace' => true, 'src' => true, 'usemap' => true, 'width' => true, ), 'ins' => array( 'datetime' => true, 'cite' => true, ), 'kbd' => array(), 'label' => array( 'for' => true, ), 'legend' => array( 'align' => true, ), 'li' => array( 'align' => true, 'value' => true, ), 'main' => array( 'align' => true, ), 'map' => array( 'name' => true, ), 'mark' => array(), 'menu' => array( 'type' => true, ), 'meter' => array( 'high' => true, 'low' => true, 'max' => true, 'min' => true, 'optimum' => true, 'value' => true, ), 'nav' => array( 'align' => true, ), 'object' => array( 'data' => array( 'required' => true, 'value_callback' => '_wp_kses_allow_pdf_objects', ), 'type' => array( 'required' => true, 'values' => array( 'application/pdf' ), ), ), 'p' => array( 'align' => true, ), 'pre' => array( 'width' => true, ), 'progress' => array( 'max' => true, 'value' => true, ), 'q' => array( 'cite' => true, ), 'rb' => array(), 'rp' => array(), 'rt' => array(), 'rtc' => array(), 'ruby' => array(), 's' => array(), 'samp' => array(), 'search' => array(), 'span' => array( 'align' => true, ), 'section' => array( 'align' => true, ), 'small' => array(), 'strike' => array(), 'strong' => array(), 'sub' => array(), 'summary' => array( 'align' => true, ), 'sup' => array(), 'table' => array( 'align' => true, 'bgcolor' => true, 'border' => true, 'cellpadding' => true, 'cellspacing' => true, 'rules' => true, 'summary' => true, 'width' => true, ), 'tbody' => array( 'align' => true, 'char' => true, 'charoff' => true, 'valign' => true, ), 'td' => array( 'abbr' => true, 'align' => true, 'axis' => true, 'bgcolor' => true, 'char' => true, 'charoff' => true, 'colspan' => true, 'headers' => true, 'height' => true, 'nowrap' => true, 'rowspan' => true, 'scope' => true, 'valign' => true, 'width' => true, ), 'textarea' => array( 'cols' => true, 'rows' => true, 'disabled' => true, 'name' => true, 'readonly' => true, ), 'tfoot' => array( 'align' => true, 'char' => true, 'charoff' => true, 'valign' => true, ), 'th' => array( 'abbr' => true, 'align' => true, 'axis' => true, 'bgcolor' => true, 'char' => true, 'charoff' => true, 'colspan' => true, 'headers' => true, 'height' => true, 'nowrap' => true, 'rowspan' => true, 'scope' => true, 'valign' => true, 'width' => true, ), 'thead' => array( 'align' => true, 'char' => true, 'charoff' => true, 'valign' => true, ), 'time' => array( 'datetime' => true, ), 'title' => array(), 'tr' => array( 'align' => true, 'bgcolor' => true, 'char' => true, 'charoff' => true, 'valign' => true, ), 'track' => array( 'default' => true, 'kind' => true, 'label' => true, 'src' => true, 'srclang' => true, ), 'tt' => array(), 'u' => array(), 'ul' => array( 'type' => true, 'popover' => true, 'role' => true, ), 'ol' => array( 'start' => true, 'type' => true, 'reversed' => true, ), 'var' => array(), 'video' => array( 'autoplay' => true, 'controls' => true, 'height' => true, 'loop' => true, 'muted' => true, 'playsinline' => true, 'poster' => true, 'preload' => true, 'src' => true, 'width' => true, ), 'wbr' => array(), ); // https://www.w3.org/TR/mathml-core/#global-attributes // Except common attributes added by _wp_add_global_attributes. $math_global_attributes = array( 'displaystyle' => true, 'scriptlevel' => true, 'mathbackground' => true, 'mathcolor' => true, 'mathsize' => true, // Common attributes also defined by _wp_add_global_attributes. // We do not want to add all those global attributes though. 'class' => true, 'data-*' => true, 'dir' => true, 'id' => true, 'style' => true, ); $math_overunder_attributes = array( 'accentunder' => true, 'accent' => true, ); $allowedposttags = array_merge( $allowedposttags, array( // https://www.w3.org/TR/mathml-core/#the-top-level-math-element 'math' => array_merge( $math_global_attributes, array( 'display' => true, ) ), // https://www.w3.org/TR/mathml-core/#token-elements // https://www.w3.org/TR/mathml-core/#text-mtext 'mtext' => $math_global_attributes, // https://www.w3.org/TR/mathml-core/#the-mi-element 'mi' => array_merge( $math_global_attributes, array( 'mathvariant' => true, ) ), // https://www.w3.org/TR/mathml-core/#number-mn 'mn' => $math_global_attributes, // https://www.w3.org/TR/mathml-core/#operator-fence-separator-or-accent-mo 'mo' => array_merge( $math_global_attributes, array( 'form' => true, 'fence' => true, 'separator' => true, 'lspace' => true, 'rspace' => true, 'stretchy' => true, 'symmetric' => true, 'maxsize' => true, 'minsize' => true, 'largeop' => true, 'movablelimits' => true, ) ), // https://www.w3.org/TR/mathml-core/#space-mspace 'mspace' => array_merge( $math_global_attributes, array( 'width' => true, 'height' => true, 'depth' => true, ) ), // https://www.w3.org/TR/mathml-core/#string-literal-ms 'ms' => $math_global_attributes, // https://www.w3.org/TR/mathml-core/#general-layout-schemata // https://www.w3.org/TR/mathml-core/#horizontally-group-sub-expressions-mrow 'mrow' => $math_global_attributes, // https://www.w3.org/TR/mathml-core/#fractions-mfrac 'mfrac' => array_merge( $math_global_attributes, array( 'linethickness' => true, ) ), // https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot 'msqrt' => $math_global_attributes, 'mroot' => $math_global_attributes, // https://www.w3.org/TR/mathml-core/#style-change-mstyle 'mstyle' => $math_global_attributes, // https://www.w3.org/TR/mathml-core/#error-message-merror 'merror' => $math_global_attributes, // https://www.w3.org/TR/mathml-core/#adjust-space-around-content-mpadded 'mpadded' => array_merge( $math_global_attributes, array( 'width' => true, 'height' => true, 'depth' => true, 'lspace' => true, 'voffset' => true, ) ), // https://www.w3.org/TR/mathml-core/#making-sub-expressions-invisible-mphantom 'mphantom' => $math_global_attributes, // https://www.w3.org/TR/mathml-core/#script-and-limit-schemata // https://www.w3.org/TR/mathml-core/#subscripts-and-superscripts-msub-msup-msubsup 'msub' => $math_global_attributes, 'msup' => $math_global_attributes, 'msubsup' => $math_global_attributes, // https://www.w3.org/TR/mathml-core/#underscripts-and-overscripts-munder-mover-munderover 'munder' => array_merge( $math_global_attributes, $math_overunder_attributes ), 'mover' => array_merge( $math_global_attributes, $math_overunder_attributes ), 'munderover' => array_merge( $math_global_attributes, $math_overunder_attributes ), // https://www.w3.org/TR/mathml-core/#prescripts-and-tensor-indices-mmultiscripts 'mmultiscripts' => $math_global_attributes, 'mprescripts' => $math_global_attributes, // https://www.w3.org/TR/mathml-core/#tabular-math // https://www.w3.org/TR/mathml-core/#table-or-matrix-mtable 'mtable' => array_merge( $math_global_attributes, array( // Non-standard, used by temml/katex. // https://developer.mozilla.org/en-US/docs/Web/MathML/Reference/Element/mtable 'columnalign' => true, 'rowspacing' => true, 'columnspacing' => true, 'align' => true, 'rowalign' => true, 'columnlines' => true, 'rowlines' => true, 'frame' => true, 'framespacing' => true, 'width' => true, ) ), // https://www.w3.org/TR/mathml-core/#row-in-table-or-matrix-mtr 'mtr' => array_merge( $math_global_attributes, array( // Non-standard, used by temml/katex. // https://developer.mozilla.org/en-US/docs/Web/MathML/Reference/Element/mtr 'columnalign' => true, 'rowalign' => true, ) ), // https://www.w3.org/TR/mathml-core/#entry-in-table-or-matrix-mtd 'mtd' => array_merge( $math_global_attributes, array( 'columnspan' => true, 'rowspan' => true, // Non-standard, used by temml/katex. // https://developer.mozilla.org/en-US/docs/Web/MathML/Reference/Element/mtd 'columnalign' => true, 'rowalign' => true, ) ), // https://www.w3.org/TR/mathml-core/#semantics-and-presentation 'semantics' => $math_global_attributes, 'annotation' => array_merge( $math_global_attributes, array( 'encoding' => true, ) ), // Non-standard but widely supported, used by temml/katex. 'menclose' => array_merge( $math_global_attributes, array( 'notation' => true, ) ), ) ); /** * @var array[] $allowedtags Array of KSES allowed HTML elements. * @since 1.0.0 */ $allowedtags = array( 'a' => array( 'href' => true, 'title' => true, ), 'abbr' => array( 'title' => true, ), 'acronym' => array( 'title' => true, ), 'b' => array(), 'blockquote' => array( 'cite' => true, ), 'cite' => array(), 'code' => array(), 'del' => array( 'datetime' => true, ), 'em' => array(), 'i' => array(), 'q' => array( 'cite' => true, ), 's' => array(), 'strike' => array(), 'strong' => array(), ); /** * @var string[] $allowedentitynames Array of KSES allowed HTML entity names. * @since 1.0.0 */ $allowedentitynames = array( 'nbsp', 'iexcl', 'cent', 'pound', 'curren', 'yen', 'brvbar', 'sect', 'uml', 'copy', 'ordf', 'laquo', 'not', 'shy', 'reg', 'macr', 'deg', 'plusmn', 'acute', 'micro', 'para', 'middot', 'cedil', 'ordm', 'raquo', 'iquest', 'Agrave', 'Aacute', 'Acirc', 'Atilde', 'Auml', 'Aring', 'AElig', 'Ccedil', 'Egrave', 'Eacute', 'Ecirc', 'Euml', 'Igrave', 'Iacute', 'Icirc', 'Iuml', 'ETH', 'Ntilde', 'Ograve', 'Oacute', 'Ocirc', 'Otilde', 'Ouml', 'times', 'Oslash', 'Ugrave', 'Uacute', 'Ucirc', 'Uuml', 'Yacute', 'THORN', 'szlig', 'agrave', 'aacute', 'acirc', 'atilde', 'auml', 'aring', 'aelig', 'ccedil', 'egrave', 'eacute', 'ecirc', 'euml', 'igrave', 'iacute', 'icirc', 'iuml', 'eth', 'ntilde', 'ograve', 'oacute', 'ocirc', 'otilde', 'ouml', 'divide', 'oslash', 'ugrave', 'uacute', 'ucirc', 'uuml', 'yacute', 'thorn', 'yuml', 'quot', 'amp', 'lt', 'gt', 'apos', 'OElig', 'oelig', 'Scaron', 'scaron', 'Yuml', 'circ', 'tilde', 'ensp', 'emsp', 'thinsp', 'zwnj', 'zwj', 'lrm', 'rlm', 'ndash', 'mdash', 'lsquo', 'rsquo', 'sbquo', 'ldquo', 'rdquo', 'bdquo', 'dagger', 'Dagger', 'permil', 'lsaquo', 'rsaquo', 'euro', 'fnof', 'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda', 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma', 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega', 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'omicron', 'pi', 'rho', 'sigmaf', 'sigma', 'tau', 'upsilon', 'phi', 'chi', 'psi', 'omega', 'thetasym', 'upsih', 'piv', 'bull', 'hellip', 'prime', 'Prime', 'oline', 'frasl', 'weierp', 'image', 'real', 'trade', 'alefsym', 'larr', 'uarr', 'rarr', 'darr', 'harr', 'crarr', 'lArr', 'uArr', 'rArr', 'dArr', 'hArr', 'forall', 'part', 'exist', 'empty', 'nabla', 'isin', 'notin', 'ni', 'prod', 'sum', 'minus', 'lowast', 'radic', 'prop', 'infin', 'ang', 'and', 'or', 'cap', 'cup', 'int', 'sim', 'cong', 'asymp', 'ne', 'equiv', 'le', 'ge', 'sub', 'sup', 'nsub', 'sube', 'supe', 'oplus', 'otimes', 'perp', 'sdot', 'lceil', 'rceil', 'lfloor', 'rfloor', 'lang', 'rang', 'loz', 'spades', 'clubs', 'hearts', 'diams', 'sup1', 'sup2', 'sup3', 'frac14', 'frac12', 'frac34', 'there4', ); /** * @var string[] $allowedxmlentitynames Array of KSES allowed XML entity names. * @since 5.5.0 */ $allowedxmlentitynames = array( 'amp', 'lt', 'gt', 'apos', 'quot', ); $allowedposttags = array_map( '_wp_add_global_attributes', $allowedposttags ); } else { $required_kses_globals = array( 'allowedposttags', 'allowedtags', 'allowedentitynames', 'allowedxmlentitynames', ); $missing_kses_globals = array(); foreach ( $required_kses_globals as $global_name ) { if ( ! isset( $GLOBALS[ $global_name ] ) || ! is_array( $GLOBALS[ $global_name ] ) ) { $missing_kses_globals[] = '$' . $global_name . ''; } } if ( $missing_kses_globals ) { _doing_it_wrong( 'wp_kses_allowed_html', sprintf( /* translators: 1: CUSTOM_TAGS, 2: Global variable names. */ __( 'When using the %1$s constant, make sure to set these globals to an array: %2$s.' ), 'CUSTOM_TAGS', implode( ', ', $missing_kses_globals ) ), '6.2.0' ); } $allowedtags = wp_kses_array_lc( $allowedtags ); $allowedposttags = wp_kses_array_lc( $allowedposttags ); } /** * Filters text content and strips out disallowed HTML. * * This function makes sure that only the allowed HTML element names, attribute * names, attribute values, and HTML entities will occur in the given text string. * * This function expects unslashed data. * * @see wp_kses_post() for specifically filtering post content and fields. * @see wp_allowed_protocols() for the default allowed protocols in link URLs. * * @see 'wp_kses_force_legacy_parser' to opt-out of the new HTML-API-based implementation. * * @since 1.0.0 * @since 7.2.0 Rewritten with the HTML API for more reliable parsing. * * @global string $wp_kses_operating_mode * * @param string $content Text content to filter. * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, * or a context name such as 'post'. {@see wp_kses_allowed_html()} * for the list of accepted context names. * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. * Defaults to the result of wp_allowed_protocols(). * @return string Filtered content containing only the allowed HTML. */ function wp_kses( $content, $allowed_html, $allowed_protocols = array() ) { global $wp_kses_operating_mode; /** * Filters whether to rely on the legacy parsing inside `wp_kses()`. * * @since 7.2.0 * * @param bool $force_legacy_parser Whether to force using the legacy parser * instead of relying on the HTML API. */ if ( ! apply_filters( 'wp_kses_force_legacy_parser', true ) ) { return wp_sanitize_html_kses( (string) $content, $allowed_html, $allowed_protocols ); } if ( empty( $allowed_protocols ) ) { $allowed_protocols = wp_allowed_protocols(); } $content = wp_kses_no_null( $content, array( 'slash_zero' => 'keep' ) ); $content = wp_kses_normalize_entities( $content ); // Ensure that the legacy pre-kses filters run by setting the operating mode appropriately. try { $previous_kses_mode = $wp_kses_operating_mode; $wp_kses_operating_mode = 'legacy'; $content = wp_kses_hook( $content, $allowed_html, $allowed_protocols ); } finally { $wp_kses_operating_mode = $previous_kses_mode; } return wp_kses_split( $content, $allowed_html, $allowed_protocols ); } /** * Filters HTML content, sanitizing according to given policies. * * Modern implementation of {@see wp_kses()} built with the HTML API. * * @since 7.2.0 * * @global string $wp_kses_operating_mode * * @access private This function should not be called directly, as it serves a transitionary * role while replacing the implementation of {@see \wp_kses()}. * * @param string $content Text content to filter. * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, * or a context name such as 'post'. See wp_kses_allowed_html() * for the list of accepted context names. * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. * Defaults to the result of wp_allowed_protocols(). * @return string Filtered content containing only the allowed HTML. */ function wp_sanitize_html_kses( $content, $allowed_html, $allowed_protocols = array() ) { global $wp_kses_operating_mode; $specified_allowed_html = $allowed_html; $allowed_protocols = empty( $allowed_protocols ) ? wp_allowed_protocols() : $allowed_protocols; // Preserve legacy behavior of stripping unwanted C0 control characters. $content = preg_replace( '/[\x01-\x08\x0B\x0C\x0E-\x1F]/', '', $content ); /* * Call legacy pre-kses filters that might have been added by plugins. * * Also set the operating mode to bypass the pre-filters from the legacy * implementation of `wp_kses()`, as these filters are now run in-band * during the processing of the input document. */ try { $previous_kses_mode = $wp_kses_operating_mode; $wp_kses_operating_mode = 'html-api'; $content = wp_kses_hook( $content, $specified_allowed_html, $allowed_protocols ); } finally { $wp_kses_operating_mode = $previous_kses_mode; } $allowed_html = is_array( $allowed_html ) ? $allowed_html : wp_kses_allowed_html( $allowed_html ); /* * The explanation for this call is that “the quoting from `preg_replace(//e)` * requires” it, but this version of `wp_kses()` doesn’t rely on PCRE functions * to parse HTML. Given that this corrupts text, it will be skipped. */ //$content = wp_kses_stripslashes( $content ); $processor = new class( $content, $specified_allowed_html, $allowed_html, $allowed_protocols, wp_kses_uri_attributes() ) extends WP_HTML_Tag_Processor { /** * An array of allowed HTML elements and attributes, or a context name such as 'post'. * * It’s important to store this alongside the resolved allowable HTML because some * filters in some plugins look for the string values, e.g. for “post” instead of * the resolved array, and apply logic based on that context. * * @see wp_kses_allowed_html() for the list of accepted context names. * @see self::$allowed_html for the resolved array of allowable HTML elements and attributes. * * @since 7.2.0 * * @var array[]|string */ private $specified_allowed_html; /** * An array of allowed HTML elements and attributes. * * This array of allowable HTML elements and attributes is resolved from the value provided * to the sanitizer function. It’s resolved at the start to avoid repeatedly calling the * filter stack and array-merging computations. However, it’s still necessary to carry along * the provided context so that filters expecting the array-or-string version continue to * operate properly. * * @see self::$specified_allowed_html * * @since 7.2.0 * * @var array[] */ private $allowed_html; /** * Array of allowed URL protocols. * * @see \wp_allowed_protocols() * * @since 7.2.0 * * @var string[] */ private $allowed_protocols; /** * List of attributes whose values are expected to be considered URLs. * * @see \wp_kses_uri_attributes() * * @since 7.2.0 * * @var string[] */ private $uri_attributes; /** * Tracks balanced tags when inside foreign content. * * @since 7.2.0 * * @var string[] */ private $foreign_content_stack = array(); /** * Tracks how deeply into a MathML ANNOTATION-XML element the current token is; * an optimization to avoid checking up the open-element stack on every token. * * @since 7.2.0 * * @var int */ private $math_annotation_xml_depth = 0; /** * Constructor function, used internally. * * @since 7.2.0 * @access private * * @param string $html Content to filter. * @param array[]|string $specified_allowed_html Context passed into sanitizer. * @param array[] $allowed_html Resolved allowable HTML. * @param string[] $allowed_protocols Resolved allowable protocols. * @param string[] $uri_attributes Resolved URI attributes. */ public function __construct( $html, $specified_allowed_html, $allowed_html, $allowed_protocols, $uri_attributes ) { parent::__construct( $html ); $this->specified_allowed_html = $specified_allowed_html; $this->allowed_html = $allowed_html; $this->allowed_protocols = $allowed_protocols; $this->uri_attributes = $uri_attributes; } /** * Returns the raw byte-span of the currently-matched token, * assuming that the parser is matched on a token. * * @since 7.2.0 * @access private * * @return WP_HTML_Span|null */ private function get_span() { $this->set_bookmark( 'here' ); if ( ! isset( $this->bookmarks['here'] ) ) { return null; } return $this->bookmarks['here']; } /** * Sets the value of a given HTML attribute, adjusting URL attributes * to ensure compatibility with legacy {@see wp_kses()} behavior. * * @since 7.2.0 * @access private * * @param string $name Name of HTML attribute to set. * @param string|true $value Value to set; `true` for boolean attribute. * @return bool Whether the attribute was set. */ public function set_attribute( $name, $value ): bool { $lower_name = strtolower( $name ); $is_url_ish = in_array( $lower_name, $this->uri_attributes, true ); if ( ! $is_url_ish || ! is_string( $value ) ) { return parent::set_attribute( $name, $value ); } $escaped = strtr( $value, array( '<' => '<', '>' => '>', '&' => '&', '"' => '"', "'" => ''', ) ); // Set a benign placeholder to replace below. if ( ! parent::set_attribute( $name, true ) ) { return false; } $this->lexical_updates[ $lower_name ]->text = " {$lower_name}=\"{$escaped}\""; return true; } /** * Indicates if the currently-matched token, given the parsed context, would require more * complicated parsing rules to proceed without risking escape from foreign content. * * @since 7.2.0 * @access private * * @param bool $is_inside_mathml_text_integration_point * @param bool $is_inside_svg_html_integration_point * @return bool */ private function could_escape_foreign_content( bool $is_inside_mathml_text_integration_point, bool $is_inside_svg_html_integration_point ): bool { $token_name = $this->get_token_name(); $is_closer = $this->is_tag_closer(); $namespace = $this->get_namespace(); $self_closing = ! $is_closer && $this->has_self_closing_flag(); if ( ! $is_closer && $is_inside_svg_html_integration_point ) { return true; } /* * These two elements are excepted in HTML from the normal processing * rules because they function in similar ways to character data. * * > The mglyph element is used to represent non-standard characters or * > symbols by images; the malignmark element establishes an alignment * > point for use within table constructs, and is otherwise invisible. * * They must contain no elements, so only allow self-closing tags. */ if ( ! $is_closer && $is_inside_mathml_text_integration_point ) { return ! ( $self_closing && ( 'MGLYPH' === $token_name || 'MALIGNMARK' === $token_name ) ); } if ( ! $is_closer && 'FONT' === $token_name && ( null !== $this->get_attribute( 'color' ) || null !== $this->get_attribute( 'face' ) || null !== $this->get_attribute( 'size' ) ) ) { return true; } if ( ! $is_closer && in_array( $token_name, array( 'B', 'BIG', 'BLOCKQUOTE', 'BODY', 'BR', 'CENTER', 'CODE', 'DD', 'DIV', 'DL', 'DT', 'EM', 'EMBED', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEAD', 'HR', 'I', 'IMG', 'LI', 'LISTING', 'MENU', 'META', 'NOBR', 'OL', 'P', 'PRE', 'RUBY', 'S', 'SMALL', 'SPAN', 'STRONG', 'STRIKE', 'SUB', 'SUP', 'TABLE', 'TT', 'U', 'UL', 'VAR', ), true ) || ( $is_closer && in_array( $token_name, array( 'BR', 'P', ), true ) ) ) { return true; } if ( 'math' === $namespace && ! $is_closer && ! $self_closing ) { $encoding = $this->get_attribute( 'encoding' ); if ( 'ANNOTATION-XML' === $token_name && is_string( $encoding ) && ( 0 === strcasecmp( $encoding, 'text/html' ) || 0 === strcasecmp( $encoding, 'application/xhtml+xml' ) ) ) { return true; } /* * When SVG becomes a direct descendant of a MathML ANNOTATION-XML, * the namespace remains `math` but there could be an SVG element * with an HTML integration point. Conservatively reject any child * SVG element inside a MathML ANNOTATION-XML to prevent this. */ if ( 'SVG' === $token_name && $this->math_annotation_xml_depth > 0 ) { return true; } } return false; } /** * Indicates if a given string contains text that would parse as a block delimiter. * * @since 7.2.0 * @access private * * @param string $text Does a block comment delimiter exist in this string value? * @return bool Whether a block comment delimiter of any kind was found in the given string. */ private static function contains_a_block_delimiter( string $text ): bool { if ( '' === $text ) { return false; } $processor = new WP_Block_Processor( $text ); return $processor->next_delimiter(); } /** * Returns a sanitized copy of the input HTML. * * @since 7.2.0 * @access private * * @return string Sanitized copy of given input HTML. */ public function sanitize() { $template_depth = 0; $output = ''; $special_newline_at = PHP_INT_MIN; $foreign_content_starts_at = PHP_INT_MAX; $open_blocks = array(); $open_blocks_at = array(); $foreign_closed_blocks = array(); $is_in_mathml_text_integration_point = false; $is_in_svg_html_integration_point = false; /** * These are treated as void elements inside the HTML API * due to the special handling of their inner text content. */ $special_atomic_elements = array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP', ); while ( $this->next_token() ) { $token_name = $this->get_token_name(); $token_type = $this->get_token_type(); $namespace = $this->get_namespace(); $is_closer = $this->is_tag_closer(); $here = $this->get_span(); /* * Prevent allowing NOSCRIPT elements whose parsing rules change * based on whether the scripting flag is enabled in a browser. * Rely on trusted inputs for producing the appropriate NOSCRIPT * content, and prevent untrusted inputs from generating it. */ if ( 'NOSCRIPT' === $token_name && ! $is_closer ) { break; } /* * PLAINTEXT is unique in that it changes the tokenization. Everything in * a document following the PLAINTEXT opening tag is interpreted as character * data, meaning there are no more tags, comments, or anything. Because of * this, it is the only element without a closing tag. * * Because of its unique role, it’s risky to leave in place because very few * parsers are going to recognize the tag and properly switch modes for the * rest of the document. Instead, the PLAINTEXT tag is removed and its content * is re-written as an escaped text node. * * This changes the structure of the document, because no PLAINTEXT element * will appear, and it won’t be possible to style its contents, but the element * is discouraged anyway and should not be allowed from untrusted inputs. */ if ( 'PLAINTEXT' === $token_name && 'html' === $namespace && ! $is_closer ) { if ( $template_depth > 0 && ! isset( $this->allowed_html['template'] ) ) { break; } $text = substr( $this->html, $here->start + $here->length ); $needs_special_newline = ( strlen( $output ) === $special_newline_at && 1 === strspn( $text, "\n\r", 0, 1 ) ); if ( $needs_special_newline ) { $output .= "\n"; } $output .= strtr( $text, array( "\x00" => "\u{FFFD}", "\r\n" => "\n", "\r" => "\n", '<' => '<', '&' => '&', '>' => '>', ) ); break; } $is_in_text_integration_point = ( $is_in_mathml_text_integration_point || ( ! $is_closer && $is_in_svg_html_integration_point ) ); /* * While content inside integration points is generally not allowed here, * character data inside the MathML text elements _is_ allowed. This is * because the rules only change slightly: NULL bytes are removed instead * of being replaced with the Unicode replacement character U+FFFD; and * active formats are reconstructed. The format reconstruction doesn’t * occur here but a browser will still do so; this sanitizer is generally * unaware of nesting structure. */ if ( $is_in_text_integration_point && '#text' === $token_type ) { $this->change_parsing_namespace( 'html' ); $text = $this->get_modifiable_text(); $this->change_parsing_namespace( $namespace ); } else { $text = $this->get_modifiable_text(); } /* * Enter the foreign content and change the parsing namespace * so that the parser recognizes real self-closing elements. */ $is_svg_or_math = 'MATH' === $token_name || 'SVG' === $token_name; $has_self_closing_flag = ! $is_closer && $this->has_self_closing_flag(); if ( $is_svg_or_math && ! $is_closer && 'html' === $namespace ) { $this->change_parsing_namespace( strtolower( $token_name ) ); $namespace = $this->get_namespace(); } if ( 'html' !== $namespace && '#tag' === $token_type ) { /* * Ensure that only well-formed foreign content is allowed. * Since un-balanced closing tags might implicitly close the * open foreign-content element, these must be rejected. */ if ( $is_closer ) { $open_element = array_pop( $this->foreign_content_stack ); if ( null === $open_element || $token_name !== $open_element ) { break; } if ( 'math' === $namespace && 'ANNOTATION-XML' === $open_element ) { --$this->math_annotation_xml_depth; } /* * Reset the foreign content tracker so it doesn’t truncate * unintentionally after foreign content has properly closed. */ if ( empty( $this->foreign_content_stack ) ) { $foreign_content_starts_at = PHP_INT_MAX; } } else { /* * Track the opening of the last transition into foreign * content so that it can be discarded when encountering * tags that would require more substantial parsing. */ if ( empty( $this->foreign_content_stack ) ) { $foreign_content_starts_at = strlen( $output ); } $this->foreign_content_stack[] = $token_name; if ( 'math' === $namespace && 'ANNOTATION-XML' === $token_name ) { ++$this->math_annotation_xml_depth; } } } if ( 'TEMPLATE' === $token_name && 'html' === $namespace && ! $is_closer ) { ++$template_depth; } $skip_token = ( ( $template_depth > 0 && ! isset( $this->allowed_html['template'] ) ) || ( ! empty( $this->foreign_content_stack ) && ! isset( $this->allowed_html[ strtolower( $this->foreign_content_stack[0] ) ] ) ) ); switch ( $token_type ) { case '#text': if ( $skip_token ) { break; } $needs_special_newline = ( strlen( $output ) === $special_newline_at && 1 === strspn( $text, "\n\r", 0, 1 ) ); $text = strtr( $text, array( "\r" => ' ', '<' => '<', '&' => '&', '>' => '>', ) ); if ( $needs_special_newline ) { $output .= "\n{$text}"; } else { $output .= $text; } break; /* * Untrusted sources should not be creating these kinds of tokens, * so remove them entirely from the output. */ case '#doctype': case '#presumptuous-tag': case '#processing-instruction': break; /* * It’s questionable whether these should be allowed through, but * the legacy behavior supports it. Therefore, allow them as long * as they don’t contain potentially confusing syntax characters. */ case '#funky-comment': if ( ! $skip_token && ! str_contains( $text, '<' ) ) { $output .= substr( $this->html, $here->start, $here->length ); } break; /* * `wp_kses()` runs iteratively on the content inside of these tokens, * but the content is benign in a browser. */ case '#comment': if ( $skip_token ) { break; } /* * There are several kinds of malformed HTML which are handled by interpreting * them as HTML comments. For example, `` is called a “bogus comment” by the * HTML specification, but when loaded by a browser is equivalent to ``. * In this way, interacting with the DOM via JavaScript differs from handling * the textual representation of a page in PHP. * * Ignore these non-normative comment forms to protect downstream parsers which * might not be expecting their kinds of syntax. This prevents mis-parses for * code which over-simplifies HTML parsing. */ if ( WP_HTML_Tag_Processor::COMMENT_AS_HTML_COMMENT !== $this->get_comment_type() ) { break; } // Apply special filtering for block comment delimiters with JSON attributes. $comment = substr( $this->html, $here->start, $here->length ); /* * A comment like `` still appears as a normative HTML comment, * but as an incorrectly-closed comment. Ignore these as well, as part of only * allowing normative comment contents. */ $was_incorrectly_closed = '!' === $comment[ strlen( $comment ) - 2 ]; if ( $was_incorrectly_closed ) { break; } $block_processor = new WP_Block_Processor( $comment ); if ( $block_processor->next_token() && ! $block_processor->is_html() ) { $block_type = $block_processor->get_block_type(); $implicit_block_type = str_starts_with( $block_type, 'core/' ) ? substr( $block_type, /* 'core/' */ 5 ) : $block_type; switch ( $block_processor->get_delimiter_type() ) { // Track when blocks open and when they don’t self-close. case WP_Block_Processor::OPENER: $open_blocks[] = $implicit_block_type; $open_blocks_at[] = strlen( $output ); break; // Track when blocks close. case WP_Block_Processor::CLOSER: if ( empty( $open_blocks ) ) { break 2; } /* * The default parser closes any open block, even when * the names don’t match. Preserve this behavior here * to avoid differences in sanitization and parsing. */ $closed_block = array_pop( $open_blocks ); $closed_block_at = array_pop( $open_blocks_at ); if ( 'html' !== $namespace && $closed_block_at < $foreign_content_starts_at ) { $foreign_closed_blocks[] = $closed_block; } } // Filter block attributes for opening delimiters. $attributes = $block_processor->allocate_and_return_parsed_attributes(); $voider = WP_Block_Processor::VOID === $block_processor->get_delimiter_type() ? '/' : ''; if ( $block_processor->opens_block() ) { if ( isset( $attributes ) ) { $attributes = filter_block_kses_value( $attributes, $this->specified_allowed_html, $this->allowed_protocols, array( 'blockName' => $block_type ) ); } } // Fully normalize all block delimiters: name, whitespace, attributes, flags. if ( WP_Block_Processor::CLOSER === $block_processor->get_delimiter_type() ) { $text = " /wp:{$implicit_block_type} "; } elseif ( ! empty( $attributes ) ) { $serialized_attributes = serialize_block_attributes( $attributes ); $text = " wp:{$implicit_block_type} {$serialized_attributes} {$voider}"; } else { $text = " wp:{$implicit_block_type} {$voider}"; } } /* * Legacy `wp_kses()` recursively calls itself on the contents of comments. * Since comment content is not escaped, this changes the meaning of those * comments when parsed. Still, code often expects to find tag-like syntax * only when they are real tags. This legacy defect is preserved to avoid * presenting content that downstream parsers might misinterpret as markup. */ $text = strtr( $text, array( '<' => '<' ) ); $output .= ""; break; /* * True CDATA sections only exist within embedded SVG and MathML content, * where they represent text data without any escaping. However, because * parsers tend to vary on how to parse these, for untrusted inputs, * rewrite all CDATA sections as normal escaped text. */ case '#cdata-section': if ( ! $skip_token ) { $output .= strtr( $text, array( "\x00" => "\u{FFFD}", '<' => '<', '&' => '&', '>' => '>', ) ); } break; case '#tag': /* * Any failures inside foreign content should return the part of * the post processed up until the entrance of the foreign content. * This is necessary because it’s only inside foreign content that * the self-closing flag indicates a self-closing element. * * While the HTML Processor can enter into SVG and MATH and track * when they close, it’s substantially more complicated and requires * considerable accounting. To avoid all of that, and to accept the * kind of content that is nominal and safe, track only when the * next tag _could_ lead to implicit changing of the parsing namespace * or insertion mode. */ if ( 'html' !== $namespace && $this->could_escape_foreign_content( $is_in_mathml_text_integration_point, $is_in_svg_html_integration_point ) ) { break 2; } if ( $skip_token ) { break; } $tag_name = strtolower( $token_name ); // Skip unallowed elements by tag name if ( ! isset( $this->allowed_html[ $tag_name ] ) ) { break; } if ( $is_closer ) { $output .= ""; break; } $is_special_atomic_element = ( 'html' === $namespace && in_array( $token_name, $special_atomic_elements, true ) ); $expects_closer = ! ( 'html' === $namespace ? ( WP_HTML_Processor::is_void( $token_name ) || $is_special_atomic_element ) : $has_self_closing_flag ); $self_closer = ( 'html' !== $namespace && $has_self_closing_flag ) ? ' /' : ''; $closing_tag = $is_special_atomic_element ? "" : ''; $attribute_names = $this->get_attribute_names_with_prefix( '' ); $element_attributes = $this->allowed_html[ $tag_name ]; // Check for required attributes. $required_attributes = array(); if ( is_array( $element_attributes ) ) { foreach ( $element_attributes as $name => $spec ) { if ( true === ( $spec['required'] ?? false ) ) { $required_attributes[ $name ] = true; } } } /* * Allow `data-*` attributes. * * When specifying `$allowed_html`, the attribute name should be set as * `data-*` (not to be mixed with the HTML 4.0 `data` attribute, see * https://www.w3.org/TR/html40/struct/objects.html#adef-data). * * Note: the attribute name should only contain `A-Za-z0-9_-` chars. */ if ( ! empty( $element_attributes['data-*'] ) ) { if ( is_array( $attribute_names ) ) { foreach ( $attribute_names as $name ) { if ( 1 === preg_match( '/^data-[a-z0-9_-]+$/', $name ) && ( ! isset( $element_attributes[ $name ] ) || '' === $element_attributes[ $name ] ) ) { $element_attributes[ $name ] = $element_attributes['data-*']; } } } unset( $element_attributes['data-*'] ); } $tag_maker = new self( "<{$tag_name}{$self_closer}>{$closing_tag}", $this->specified_allowed_html, $this->allowed_html, $this->allowed_protocols, $this->uri_attributes ); $tag_maker->change_parsing_namespace( $namespace ); $tag_maker->next_token(); if ( is_array( $attribute_names ) ) { foreach ( $attribute_names as $name ) { $spec = $element_attributes[ $name ] ?? null; // This attribute is not specified, thus not allowed. Skip it. if ( null === $spec || '' === $spec ) { continue; } $raw_value = $this->get_attribute( $name ); $value = is_string( $raw_value ) ? $raw_value : ''; // Process the style attribute through CSS sanitization. if ( 'style' === $name ) { if ( ! is_string( $raw_value ) ) { continue; } $value = safecss_filter_attr( $value ); if ( '' === trim( $value ) ) { continue; } } $is_url_ish = in_array( strtolower( $name ), $this->uri_attributes, true ); if ( $is_url_ish ) { $value = wp_kses_bad_protocol( $value, $this->allowed_protocols ); } /* * Process the remaining attributes according to their policies. * * Non-array values for the attribute specification are assumed * to be `true`, thus permitting the attribute. */ if ( is_array( $spec ) ) { foreach ( $spec as $property => $constraint ) { $vless = true === $raw_value ? 'y' : 'n'; if ( ! wp_kses_check_attr_val( $value, $vless, $property, $constraint ) ) { continue 2; } } } $did_set = ( true === $raw_value && '' === $value ) ? $tag_maker->set_attribute( $name, true ) : $tag_maker->set_attribute( $name, $value ); if ( $did_set ) { unset( $required_attributes[ $name ] ); } } } $needs_special_newline = 'html' === $namespace && ( 'PRE' === $token_name || 'LISTING' === $token_name ); if ( ! empty( $required_attributes ) ) { if ( ! $expects_closer ) { break; } /* * Since this processor cannot track nesting of HTML elements * generally, leave opening tags when required attributes are * missing, but strip them of their attributes. */ $output .= "<{$tag_name}>"; if ( $needs_special_newline ) { $special_newline_at = strlen( $output ); } break; } if ( $is_special_atomic_element ) { if ( 'TITLE' === $token_name || 'TEXTAREA' === $token_name ) { /* * RCDATA nodes can be safely escaped, but this must be * done after enqueing the update to avoid double-escaping. */ $tag_maker->set_modifiable_text( $text ); $tag_maker->lexical_updates['modifiable text']->text = strtr( $tag_maker->lexical_updates['modifiable text']->text, array( "\x00" => "\u{FFFD}", "\r" => ' ', ) ); } elseif ( ! self::contains_a_block_delimiter( $text ) ) { // Other nodes not containing a block delimiter are safe. $tag_maker->set_modifiable_text( $text ); } else { /* * But RAWTEXT and SCRIPT cannot be generally escaped, so reject * updates which would include something that could be misparsed * as a block comment delimiter. */ $tag_maker->set_modifiable_text( '' ); } } $output .= $tag_maker->get_updated_html(); if ( $needs_special_newline ) { $special_newline_at = strlen( $output ); } break; } // Re-enter the HTML namespace. if ( 'html' !== $namespace ) { if ( $has_self_closing_flag ) { array_pop( $this->foreign_content_stack ); if ( 'math' === $namespace && 'ANNOTATION-XML' === $token_name ) { --$this->math_annotation_xml_depth; } } if ( empty( $this->foreign_content_stack ) ) { $is_in_mathml_text_integration_point = false; $is_in_svg_html_integration_point = false; $this->change_parsing_namespace( 'html' ); $foreign_content_starts_at = PHP_INT_MAX; $foreign_closed_blocks = array(); } elseif ( '#tag' === $token_type && ! $has_self_closing_flag ) { switch ( $token_name ) { case 'MI': case 'MN': case 'MO': case 'MS': case 'MTEXT': $is_in_mathml_text_integration_point = ! $is_closer && 'math' === $namespace; break; case 'DESC': case 'FOREIGNOBJECT': case 'TITLE': $is_in_svg_html_integration_point = ! $is_closer && 'svg' === $namespace; break; } } } if ( 'TEMPLATE' === $token_name && $template_depth > 0 && $is_closer && 'html' === $namespace ) { --$template_depth; } } /* * While there might have been an incomplete token in the output stream, * there is no need to render it to the output. They would disappear on * their own in a browser if they ended the document, but here they do * not end the document; instead, they are likely being inserted into an * existing document, where the incomplete token might mess with the rest * of the page’s HTML structure. */ $sanitized = substr( $output, 0, $foreign_content_starts_at ); // Templates should be contained within blocks, so close them first. if ( $template_depth > 0 && isset( $this->allowed_html['template'] ) ) { $sanitized .= str_repeat( '', $template_depth ); } // Close any remaining-open blocks ensure isolation of block content. foreach ( $foreign_closed_blocks as $block_name ) { $sanitized .= ""; } for ( $i = count( $open_blocks ) - 1; $i >= 0; $i-- ) { // Skip blocks that were opened when inside truncated foreign content. if ( $open_blocks_at[ $i ] >= $foreign_content_starts_at ) { continue; } $block_name = $open_blocks[ $i ]; $sanitized .= ""; } return $sanitized; } }; return $processor->sanitize(); } /** * Filters one HTML attribute and ensures its value is allowed. * * This function can escape data in some situations where `wp_kses()` must strip the whole attribute. * * @since 4.2.3 * * @param string $attr The 'whole' attribute, including name and value. * @param string $element The HTML element name to which the attribute belongs. * @return string Filtered attribute. */ function wp_kses_one_attr( $attr, $element ) { $uris = wp_kses_uri_attributes(); $allowed_html = wp_kses_allowed_html( 'post' ); $allowed_protocols = wp_allowed_protocols(); $attr = wp_kses_no_null( $attr, array( 'slash_zero' => 'keep' ) ); // Preserve leading and trailing whitespace. $matches = array(); preg_match( '/^\s*/', $attr, $matches ); $lead = $matches[0]; preg_match( '/\s*$/', $attr, $matches ); $trail = $matches[0]; if ( empty( $trail ) ) { $attr = substr( $attr, strlen( $lead ) ); } else { $attr = substr( $attr, strlen( $lead ), -strlen( $trail ) ); } // Parse attribute name and value from input. $split = preg_split( '/\s*=\s*/', $attr, 2 ); $name = $split[0]; if ( count( $split ) === 2 ) { $value = $split[1]; /* * Remove quotes surrounding $value. * Also guarantee correct quoting in $attr for this one attribute. */ if ( '' === $value ) { $quote = ''; } else { $quote = $value[0]; } if ( '"' === $quote || "'" === $quote ) { if ( ! str_ends_with( $value, $quote ) ) { return ''; } $value = substr( $value, 1, -1 ); } else { $quote = '"'; } // Sanitize quotes, angle braces, and entities. $value = esc_attr( $value ); // Sanitize URI values. if ( in_array( strtolower( $name ), $uris, true ) ) { $value = wp_kses_bad_protocol( $value, $allowed_protocols ); } $attr = "$name=$quote$value$quote"; $vless = 'n'; } else { $value = ''; $vless = 'y'; } // Sanitize attribute by name. wp_kses_attr_check( $name, $value, $attr, $vless, $element, $allowed_html ); // Restore whitespace. return $lead . $attr . $trail; } /** * Returns an array of allowed HTML tags and attributes for a given context. * * @since 3.5.0 * @since 5.0.1 `form` removed as allowable HTML tag. * * @global array $allowedposttags * @global array $allowedtags * @global array $allowedentitynames * * @param string|array $context The context for which to retrieve tags. Allowed values are 'post', * 'strip', 'data', 'entities', or the name of a field filter such as * 'pre_user_description', or an array of allowed HTML elements and attributes. * @return array Array of allowed HTML tags and their allowed attributes. */ function wp_kses_allowed_html( $context = '' ) { global $allowedposttags, $allowedtags, $allowedentitynames; if ( is_array( $context ) ) { // When `$context` is an array it's actually an array of allowed HTML elements and attributes. $html = $context; $context = 'explicit'; /** * Filters the HTML tags that are allowed for a given context. * * HTML tags and attribute names are case-insensitive in HTML but must be * added to the KSES allow list in lowercase. An item added to the allow list * in upper or mixed case will not recognized as permitted by KSES. * * @since 3.5.0 * * @param array[] $html Allowed HTML tags. * @param string $context Context name. */ return apply_filters( 'wp_kses_allowed_html', $html, $context ); } switch ( $context ) { case 'post': /** This filter is documented in wp-includes/kses.php */ $tags = apply_filters( 'wp_kses_allowed_html', $allowedposttags, $context ); // 5.0.1 removed the `
` tag, allow it if a filter is allowing it's sub-elements `` or `