Make WordPress Core

Opened 6 months ago

Last modified 5 days ago

#63724 reopened enhancement

HTML API: Reliably parse HTML attributes in `wp_kses_hair()`

Reported by: dmsnell's profile dmsnell Owned by: dmsnell's profile dmsnell
Milestone: 7.0 Priority: normal
Severity: normal Version: 6.9
Component: HTML API Keywords: has-patch has-unit-tests needs-refresh
Focuses: Cc:

Description

wp_kses_hair() attempts to parse HTML attributes given the span of text inside an HTML tag, but excluding the tag name, opening <, and closing >. For example:

<?php
$attrs = wp_kses_hair( ' class="description"', wp_allowed_protocols() );
$attrs === array(
        'class' => array(
                'name'  => 'class',
                'value' => 'description',
                'whole' => 'class="description"',
                'vless' => 'n',
        )
);

While this has served WordPress for years, there are fundamental issues in the parsing model; namely, that it isn’t spec-compliant with the HTML5 living standard. Categories of legitimate attribute values are rejected and overlooked, while certain values are misinterpreted as something other than what they are.

Ideally, there would be no need for this function:

  • Passing in the string of text inside the HTML tags covered by the attributes is awkward and carries forward parsing errors in determining what that string is.
  • The output format does not decode attribute values, which passes along confusion about whether content is escaped or not.
  • The HTML API provides more efficient and reliable tools to work directly with HTML and not have to split it and pass around sub-spans of whole HTML tokens.

However, use of the function is pervasive in the plugin space and so the function must remain.

WordPress should lean on the reliability that the HTML API affords to properly parse attributes inside of wp_kses_hair() as part of a push away from ad-hoc HTML parsing and towards reliance on the HTML API.

Change History (16)

This ticket was mentioned in PR #9248 on WordPress/wordpress-develop by @dmsnell.


6 months ago
#1

  • Keywords has-patch added

Trac ticket: Core-63724

Replaces #7407, dmsnell#5
Coordination in #9256

wp_kses_hair() is built around an impressive state machine for parsing the $attr of an HTML tag, that is, the span of text after the tag name and before the closing >. Unfortunately, that parsing code doesn’t fully-implement the HTML specification and may be prone to mis-parsing.

This patch replaces the existing state machine with a straight-forward use of the HTML API to parse the attributes for us, constructing a shell take for the $attr string and reading the attributes structurally. This shell is necessary because a previous stage of the pipeline has already separated what it thinks is the so-called “attribute list” from a tag.

## Dependencies

#2 @jorbin
6 months ago

  • Keywords needs-unit-tests added

With the noted pervasiveness of this function by plugins, I think that this needs some unit tests that can show the behavior isn't changing, or if it does change it is minimally done in documented and understandable ways which can be messaged.

#3 @dmsnell
6 months ago

Thanks @jorbin — I’m slow, but I plan on adding tests that assert the mapping between HTML attributes and the kinds of outputs that wp_kses_hair() produces.

This ticket will probably be a good proving ground for these kinds of changes, as there is a long list of core functionality that I would like to see us update over the coming years. Almost every one of them will involve behavioral changes, but I believe that leaving the kinds of breakages in place is not the best way forward.

At least in the ideal cases the behaviors are preserved. That is, the intent matches. I don’t know how much belongs in a test to show those changes, but I know some of those tests will lose relevance with time. For example, if wp_kses_hair() misparses an HTML attribute but WordPress now properly understands that attribute, do you have suggestions on how you would like to see that?

At the extreme end I know we don’t document previous bugs, but as you say, this has been long established. Still, in some cases, logging notable changes is also broadcasting some sensitive vulnerabilities that remain in other parts of Core.

This ticket was mentioned in Slack in #core by welcher. View the logs.


4 months ago

#5 @welcher
4 months ago

@dmsnell this was mentioned in the scrub today. Do you think you'll be able to pick this up for 6.9?

This ticket was mentioned in Slack in #core by benjamin_zekavica. View the logs.


4 months ago

This ticket was mentioned in PR #10142 on WordPress/wordpress-develop by @jignesh.nakrani.


3 months ago
#7

  • Keywords has-unit-tests added; needs-unit-tests removed

#8 @westonruter
3 months ago

  • Owner set to dmsnell
  • Status changed from new to assigned

#9 @wildworks
3 months ago

  • Keywords needs-refresh added
  • Milestone changed from 6.9 to 7.0

The 6.9 Beta1 release is coming soon, so I'd like to punt this ticket to 7.0. Also, both PRs have unit test failures that require updates.

#10 @dmsnell
3 months ago

@wildworks @welcher hard to tell. I can give an update within the next few hours hopefully

@jonsurrell commented on PR #9248:


6 days ago
#11

This function had quirks that change with this PR and I want to understand them.

I created a test suite for wp_kses_hair(), then I merged this branch and updated to get a diff of test changes. I also looked at several of the most popular results from WP Directory to understand usage.

My review of the most common usages on suggest that _this change is safe to make and would not negatively impact plugin authors_.

  • Historically the value and whole properties of the returned array indicate the raw parsed bytes from the HTML (with some exceptions). This means that HTML character references are not decoded. This represents an abstraction leak between the HTML and structural return value.
  • - Should this refactor leave the messy return values in place or should it decode the attribute values to enforce the view of the world developers are imagining when calling it? (that all values are normal PHP strings and not HTML text node strings)?

This is a tricky question. It doesn't _seem_ like folks rely on specifics of the input representation being present in the output, however it's certainly possible.

In one of the examples from plugins, esc_attr() is called on the attribute value to construct a new HTML string. This should be perfectly fine because the original HTML was re-encoded in this PR and esc_attr() will avoid double-encoding. They also statically wrap with ", which made the esc_attr() necessary because the attribute value could have contained "!

After some reflection, I believe the behavior you've implemented here _is a good decision_. Consider that the input is HTML and the output (value and whole) have always been some form of HTML. The difference here is a _normalization_ of the HTML in the output.

---

<details>

<summary>behavior diff</summary>

  • tests/phpunit/tests/kses/wpKsesHair.php

    diff --git a/tests/phpunit/tests/kses/wpKsesHair.php b/tests/phpunit/tests/kses/wpKsesHair.php
    index 2ed83679f2e3d..05d573bc070bc 100644
    a b public function data_attribute_parsing() { 
    5757                                'title' => array(
    5858                                        'name'  => 'title',
    5959                                        'value' => 'My Title',
    60                                         'whole' => "title='My Title'",
     60                                        'whole' => 'title="My Title"',
    6161                                        'vless' => 'n',
    6262                                ),
    6363                        ),
    public function data_attribute_parsing() { 
    188188                        array(
    189189                                'title' => array(
    190190                                        'name'  => 'title',
    191                                         'value' => '&#60;test&#62;',
    192                                         'whole' => 'title="&#60;test&#62;"',
     191                                        'value' => '&lt;test&gt;',
     192                                        'whole' => 'title="&lt;test&gt;"',
    193193                                        'vless' => 'n',
    194194                                ),
    195195                        ),
    public function data_attribute_parsing() { 
    200200                        array(
    201201                                'title' => array(
    202202                                        'name'  => 'title',
    203                                         'value' => '&#x3C;hex&#x3E;',
    204                                         'whole' => 'title="&#x3C;hex&#x3E;"',
     203                                        'value' => '&lt;hex&gt;',
     204                                        'whole' => 'title="&lt;hex&gt;"',
    205205                                        'vless' => 'n',
    206206                                ),
    207207                        ),
    public function data_attribute_parsing() { 
    212212                        array(
    213213                                'title' => array(
    214214                                        'name'  => 'title',
    215                                         'value' => '&#X3C;HEX&#X3E;',
    216                                         'whole' => 'title="&#X3C;HEX&#X3E;"',
     215                                        'value' => '&lt;HEX&gt;',
     216                                        'whole' => 'title="&lt;HEX&gt;"',
    217217                                        'vless' => 'n',
    218218                                ),
    219219                        ),
    public function data_attribute_parsing() { 
    224224                        array(
    225225                                'title' => array(
    226226                                        'name'  => 'title',
    227                                         'value' => '&invalid; &#; &#x;',
    228                                         'whole' => 'title="&invalid; &#; &#x;"',
     227                                        'value' => '&amp;invalid; &amp;#; &amp;#x;',
     228                                        'whole' => 'title="&amp;invalid; &amp;#; &amp;#x;"',
    229229                                        'vless' => 'n',
    230230                                ),
    231231                        ),
    public function data_attribute_parsing() { 
    249249                                'data-text' => array(
    250250                                        'name'  => 'data-text',
    251251                                        'value' => 'Single quoted value',
    252                                         'whole' => "data-text='Single quoted value'",
     252                                        'whole' => 'data-text="Single quoted value"',
    253253                                        'vless' => 'n',
    254254                                ),
    255255                        ),
    public function data_attribute_parsing() { 
    267267                                'alt'   => array(
    268268                                        'name'  => 'alt',
    269269                                        'value' => 'single',
    270                                         'whole' => "alt='single'",
     270                                        'whole' => 'alt="single"',
    271271                                        'vless' => 'n',
    272272                                ),
    273273                                'id'    => array(
    public function data_attribute_parsing() { 
    284284                        array(
    285285                                'title' => array(
    286286                                        'name'  => 'title',
    287                                         'value' => "It's working",
    288                                         'whole' => 'title="It\'s working"',
     287                                        'value' => 'It&apos;s working',
     288                                        'whole' => 'title="It&apos;s working"',
    289289                                        'vless' => 'n',
    290290                                ),
    291291                        ),
    public function data_attribute_parsing() { 
    296296                        array(
    297297                                'title' => array(
    298298                                        'name'  => 'title',
    299                                         'value' => 'He said "hello"',
    300                                         'whole' => 'title=\'He said "hello"\'',
     299                                        'value' => 'He said &quot;hello&quot;',
     300                                        'whole' => 'title="He said &quot;hello&quot;"',
    301301                                        'vless' => 'n',
    302302                                ),
    303303                        ),
    public function data_attribute_parsing() { 
    327327
    328328                yield 'invalid attribute name starting with number' => array(
    329329                        '1invalid="value"',
    330                         array(),
     330                        array(
     331                                '1invalid' => array(
     332                                        'name'  => '1invalid',
     333                                        'value' => 'value',
     334                                        'whole' => '1invalid="value"',
     335                                        'vless' => 'n',
     336                                ),
     337                        ),
    331338                );
    332339
    333340                yield 'invalid attribute name special chars' => array(
    334341                        '@invalid="value" $bad="value"',
    335                         array(),
     342                        array(
     343                                '@invalid' => array(
     344                                        'name'  => '@invalid',
     345                                        'value' => 'value',
     346                                        'whole' => '@invalid="value"',
     347                                        'vless' => 'n',
     348                                ),
     349                                '$bad'     => array(
     350                                        'name'  => '$bad',
     351                                        'value' => 'value',
     352                                        'whole' => '$bad="value"',
     353                                        'vless' => 'n',
     354                                ),
     355                        ),
    336356                );
    337357
    338358                yield 'duplicate attributes first wins' => array(
    public function data_attribute_parsing() { 
    355375
    356376                yield 'malformed unclosed double quote' => array(
    357377                        'title="unclosed class="test"',
    358                         array(),
     378                        array(
     379                                'title' => array(
     380                                        'name'  => 'title',
     381                                        'value' => 'unclosed class=',
     382                                        'whole' => 'title="unclosed class="',
     383                                        'vless' => 'n',
     384                                ),
     385                                'test"' => array(
     386                                        'name'  => 'test"',
     387                                        'value' => '',
     388                                        'whole' => 'test"',
     389                                        'vless' => 'y',
     390                                ),
     391                        ),
    359392                );
    360393
    361394                yield 'very long attribute value' => array(
    public function data_attribute_parsing() { 
    610643                                'alt'   => array(
    611644                                        'name'  => 'alt',
    612645                                        'value' => '',
    613                                         'whole' => "alt=''",
     646                                        'whole' => 'alt=""',
    614647                                        'vless' => 'n',
    615648                                ),
    616649                                'class' => array(
    public function data_attribute_parsing() { 
    625658                yield 'forward slashes between attributes' => array(
    626659                        'att / att2=2 /// att3="3"',
    627660                        array(
    628                                 'att'   => array(
     661                                'att'  => array(
    629662                                        'name'  => 'att',
    630663                                        'value' => '',
    631664                                        'whole' => 'att',
    public function data_attribute_parsing() { 
    652685                                'att'  => array(
    653686                                        'name'  => 'att',
    654687                                        'value' => 'val',
    655                                         'whole' => "att='val'",
     688                                        'whole' => 'att="val"',
    656689                                        'vless' => 'n',
    657690                                ),
    658691                                'att2' => array(
    659692                                        'name'  => 'att2',
    660693                                        'value' => 'val2',
    661                                         'whole' => "att2='val2'",
     694                                        'whole' => 'att2="val2"',
    662695                                        'vless' => 'n',
    663696                                ),
    664697                        ),
    public function data_attribute_parsing() { 
    670703                                'att'  => array(
    671704                                        'name'  => 'att',
    672705                                        'value' => 'val',
    673                                         'whole' => "att='val'",
     706                                        'whole' => 'att="val"',
    674707                                        'vless' => 'n',
    675708                                ),
    676709                                'att2' => array(
    677710                                        'name'  => 'att2',
    678711                                        'value' => 'val2',
    679                                         'whole' => "att2='val2'",
     712                                        'whole' => 'att2="val2"',
    680713                                        'vless' => 'n',
    681714                                ),
    682715                        ),
    public function data_attribute_parsing() { 
    688721                                'att'  => array(
    689722                                        'name'  => 'att',
    690723                                        'value' => 'val',
    691                                         'whole' => "att='val'",
     724                                        'whole' => 'att="val"',
    692725                                        'vless' => 'n',
    693726                                ),
    694727                                'att2' => array(
    695728                                        'name'  => 'att2',
    696729                                        'value' => 'val2',
    697                                         'whole' => "att2='val2'",
     730                                        'whole' => 'att2="val2"',
    698731                                        'vless' => 'n',
    699732                                ),
    700733                        ),
    public function data_attribute_parsing() { 
    706739                                'att'  => array(
    707740                                        'name'  => 'att',
    708741                                        'value' => 'val',
    709                                         'whole' => "att='val'",
     742                                        'whole' => 'att="val"',
    710743                                        'vless' => 'n',
    711744                                ),
    712745                                'att2' => array(
    713746                                        'name'  => 'att2',
    714747                                        'value' => 'val2',
    715                                         'whole' => "att2='val2'",
     748                                        'whole' => 'att2="val2"',
    716749                                        'vless' => 'n',
    717750                                ),
    718751                        ),
    public function data_attribute_parsing() { 
    739772                // Malformed Equals Patterns.
    740773                yield 'multiple equals signs' => array(
    741774                        'att=="val"',
    742                         array(),
     775                        array(
     776                                'att' => array(
     777                                        'name'  => 'att',
     778                                        'value' => '=&quot;val&quot;',
     779                                        'whole' => 'att="=&quot;val&quot;"',
     780                                        'vless' => 'n',
     781                                ),
     782                        ),
    743783                );
    744784
    745785                yield 'equals with strange spacing' => array(
    746786                        'att= ="val"',
    747                         array(),
     787                        array(
     788                                'att' => array(
     789                                        'name'  => 'att',
     790                                        'value' => '=&quot;val&quot;',
     791                                        'whole' => 'att="=&quot;val&quot;"',
     792                                        'vless' => 'n',
     793                                ),
     794                        ),
    748795                );
    749796
    750797                yield 'triple equals signs' => array(
    751798                        'att==="val"',
    752                         array(),
     799                        array(
     800                                'att' => array(
     801                                        'name'  => 'att',
     802                                        'value' => '==&quot;val&quot;',
     803                                        'whole' => 'att="==&quot;val&quot;"',
     804                                        'vless' => 'n',
     805                                ),
     806                        ),
    753807                );
    754808
    755809                yield 'equals echo pattern' => array(
    756810                        "att==echo 'something'",
    757811                        array(
    758                                 'att' => array(
     812                                'att'         => array(
    759813                                        'name'  => 'att',
    760814                                        'value' => '=echo',
    761815                                        'whole' => 'att="=echo"',
    762816                                        'vless' => 'n',
    763817                                ),
     818                                "'something'" => array(
     819                                        'name'  => "'something'",
     820                                        'value' => '',
     821                                        'whole' => "'something'",
     822                                        'vless' => 'y',
     823                                ),
    764824                        ),
    765825                );
    766826
    767827                yield 'attribute starting with equals' => array(
    768828                        '= bool k=v',
    769829                        array(
     830                                '='    => array(
     831                                        'name'  => '=',
     832                                        'value' => '',
     833                                        'whole' => '=',
     834                                        'vless' => 'y',
     835                                ),
    770836                                'bool' => array(
    771837                                        'name'  => 'bool',
    772838                                        'value' => '',
    public function data_attribute_parsing() { 
    785851                yield 'mixed quotes and equals chaos' => array(
    786852                        'k=v ="' . "' j=w",
    787853                        array(
    788                                 'k' => array(
     854                                'k'        => array(
    789855                                        'name'  => 'k',
    790856                                        'value' => 'v',
    791857                                        'whole' => 'k="v"',
    792858                                        'vless' => 'n',
    793859                                ),
     860                                '="' . "'" => array(
     861                                        'name'  => '="' . "'",
     862                                        'value' => '',
     863                                        'whole' => '="' . "'",
     864                                        'vless' => 'y',
     865                                ),
     866                                'j'        => array(
     867                                        'name'  => 'j',
     868                                        'value' => 'w',
     869                                        'whole' => 'j="w"',
     870                                        'vless' => 'n',
     871                                ),
    794872                        ),
    795873                );
    796874
    797875                yield 'triple equals quoted whitespace' => array(
    798876                        '==="  "',
    799                         array(),
     877                        array(
     878                                '=' => array(
     879                                        'name'  => '=',
     880                                        'value' => '=&quot;',
     881                                        'whole' => '=="=&quot;"',
     882                                        'vless' => 'n',
     883                                ),
     884                                '"' => array(
     885                                        'name'  => '"',
     886                                        'value' => '',
     887                                        'whole' => '"',
     888                                        'vless' => 'y',
     889                                ),
     890                        ),
    800891                );
    801892
    802893                yield 'boolean with contradictory value' => array(
    public function data_attribute_parsing() { 
    820911                yield 'empty attribute name with value' => array(
    821912                        '="value" class="test"',
    822913                        array(
    823                                 'class' => array(
     914                                '="value"' => array(
     915                                        'name'  => '="value"',
     916                                        'value' => '',
     917                                        'whole' => '="value"',
     918                                        'vless' => 'y',
     919                                ),
     920                                'class'    => array(
    824921                                        'name'  => 'class',
    825922                                        'value' => 'test',
    826923                                        'whole' => 'class="test"',
    public function data_protocol_filtering() { 
    890987                                'href' => array(
    891988                                        'name'  => 'href',
    892989                                        'value' => 'alert(1)',
    893                                         'whole' => "href='alert(1)'",
     990                                        'whole' => 'href="alert(1)"',
    894991                                        'vless' => 'n',
    895992                                ),
    896993                        ),
    public function data_protocol_filtering() { 
    9251022                        array(
    9261023                                'src' => array(
    9271024                                        'name'  => 'src',
    928                                         'value' => 'text/html,<script>alert(1)</script>',
    929                                         'whole' => 'src="text/html,<script>alert(1)</script>"',
     1025                                        'value' => 'text/html,&lt;script&gt;alert(1)&lt;/script&gt;',
     1026                                        'whole' => 'src="text/html,&lt;script&gt;alert(1)&lt;/script&gt;"',
    9301027                                        'vless' => 'n',
    9311028                                ),
    9321029                        ),

</details>

Here are two examples from the most most popular plugins in the WP Directory search:

From YITH (this appears to be part of the yith library used in many of their plugins):

/**
         * Transform attributes array to HTML attributes string.
         * If using a string, the attributes will be escaped.
         * Prefer using arrays.
         *
         * @param array|string $attributes The attributes.
         * @param bool         $echo       Set to true to print it directly; false otherwise.
         *
         * @return string
         * @since 3.7.0
         * @since 3.8.0 Escaping attributes when using strings; allow value-less attributes by setting value to null.
         */
        function yith_plugin_fw_html_attributes_to_string( $attributes = array(), $echo = false ) {
                $html_attributes = '';


                if ( ! ! $attributes ) {
                        if ( is_string( $attributes ) ) {
                                $parsed_attrs = wp_kses_hair( $attributes, wp_allowed_protocols() );
                                $attributes   = array();
                                foreach ( $parsed_attrs as $attr ) {
                                        $attributes[ $attr['name'] ] = 'n' === $attr['vless'] ? $attr['value'] : null;
                                }
                        }


                        if ( is_array( $attributes ) ) {
                                $html_attributes = array();
                                foreach ( $attributes as $key => $value ) {
                                        if ( ! is_null( $value ) ) {
                                                $html_attributes[] = esc_attr( $key ) . '="' . esc_attr( $value ) . '"';
                                        } else {
                                                $html_attributes[] = esc_attr( $key );
                                        }
                                }
                                $html_attributes = implode( ' ', $html_attributes );
                        }
                }


                if ( $echo ) {
                        // Already escaped above.
                        echo $html_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                }


                return $html_attributes;
        }

And Jetpack:

$params = wp_kses_hair( $params, array( 'http' ) );


                                $width  = isset( $params['width'] ) ? (int) $params['width']['value'] : 0;
                                $height = isset( $params['height'] ) ? (int) $params['height']['value'] : 0;
                                $wh     = '';


                                if ( $width && $height ) {
                                        $wh = "&w=$width&h=$height";
                                }


                                $url = esc_url_raw( "https://www.youtube.com/watch?v={$match[3]}{$wh}" );

#12 @jonsurrell
6 days ago

The implementation in PR 9248 is great. It's a big simplification and it brings clarity and rigor to this function. Frankly, I like it.

Ideally, there would be no need for this function.

The proposed change is good. But should we bother touching this function at all? What about just deprecating it and suggesting folks use the HTML API?

#13 @dmsnell
6 days ago

What about just deprecating it and suggesting folks use the HTML API?

I think it will take more time to eradicate its use from within Core. at that point I’m all for deprecating it. Until then, my take is that it’s worth improving the reliability. Some of the examples you shared highlight critical categories of inputs where Core is currently confused.

#14 @dmsnell
6 days ago

@jorbin thanks to @jonsurrell’s work we have a test suite, and it demonstrates the behaviors as well as where they different with this change.

I’m going to merge this, expecting to watch things and revert if necessary, but I think it will be stable. None of the cases that were previously broken were part of the function contract, and additionally, calling code had to already expect proper results.

This is hard for me to verbalize, but here is an example. Suppose we had id=&#x3c; as our input. In any situation that code previously wanted to detect something about this pattern, it already had to also accept id="<", id="&lt;", id='&lt;', and a few other variants.

So I think this change is not presenting any meaningful differences in expectations but rather normalizing inputs so that only a subset of the pre-existing expectations are necessary. It’s shrinking the domain of required support.

The test suite shows some great examples of updates that definitely change behavior but which are also definitely wanted: in many cases Core is entirely unaware of the presence of existing attributes and can lead calling code to duplicate attribute or defeat valuable checks because of a presumed absence or misparse.

The original description on the function suggests it will perform normalization, but now it will be done comprehensively.

#15 @dmsnell
5 days ago

  • Resolution set to fixed
  • Status changed from assigned to closed

In 61467:

HTML API: Refactor wp_kses_hair() for spec-compliance.

wp_kses_hair() is built around an impressive state machine for parsing the span of text following an HTML tag name and the tag’s closing > into a structured representation of the attributes. Unfortunately that parsing code doesn’t comply with the HTML Living Standard and is prone to mis-parsing attributes, particularly in the presence of malformed inputs.

This patch replaces the existing state machine with the spec-compliant parsing from the HTML API. With a comprehensive test suite covering attribute parsing, the same reliability the Tag Processor affords will be applied to wp_kses_hair(), giving new guarantees not previously available in Core:

  • All attribute values are reported fully-normalized, where character references are decoded and then re-encoded in a predictable manner. Only the “big five” syntax characters (“&<>'"”) will remain, and in their named forms.
  • All whole values are fully normalized and presented either as boolean attributes without a value, or with double-quoted attribute values.
  • All attributes and their values will be properly parsed according to how a browser would parse them, bringing agreement between the server and user agents.

Developed in https://github.com/WordPress/wordpress-develop/pull/9248
Discussed in https://core.trac.wordpress.org/ticket/63724

Props adamziel, dmsnell, jonsurrell, jorbin, westonruter.
Fixes #63724.

#16 @dmsnell
5 days ago

  • Resolution fixed deleted
  • Status changed from closed to reopened

Re-opening to track follow-up work:

  • Apply the same basic fix to wp_kses_hair_parse().
  • Deprecate wp_kses_html_error()
Note: See TracTickets for help on using tickets.