Skip to content

Commit 37386b4

Browse files
committed
KSES: Allow all Custom Data Attributes
Allow spec-compliant data-attributes in `wp_kses_attr_check()`
1 parent 1885bee commit 37386b4

File tree

2 files changed

+118
-26
lines changed

2 files changed

+118
-26
lines changed

src/wp-includes/kses.php

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,24 +1349,27 @@ function wp_kses_attr_check( &$name, &$value, &$whole, $vless, $element, $allowe
13491349

13501350
$allowed_attr = $allowed_html[ $element_low ];
13511351

1352+
/*
1353+
* Allow Custom Data Attributes (`data-*`).
1354+
*
1355+
* When specifying `$allowed_html`, the attribute name should be set as
1356+
* `data-*` (not to be mixed with the HTML 4.0 `data` attribute, see
1357+
* https://www.w3.org/TR/html40/struct/objects.html#adef-data).
1358+
*
1359+
* Custom data attributes appear on an HTML element in the `dataset`
1360+
* property and are available from JavaScript with a transformed name.
1361+
*
1362+
* @see https://html.spec.whatwg.org/#custom-data-attribute
1363+
*/
13521364
if ( ! isset( $allowed_attr[ $name_low ] ) || '' === $allowed_attr[ $name_low ] ) {
1353-
/*
1354-
* Allow `data-*` attributes.
1355-
*
1356-
* When specifying `$allowed_html`, the attribute name should be set as
1357-
* `data-*` (not to be mixed with the HTML 4.0 `data` attribute, see
1358-
* https://www.w3.org/TR/html40/struct/objects.html#adef-data).
1359-
*
1360-
* Note: the attribute name should only contain `A-Za-z0-9_-` chars.
1361-
*/
1362-
if ( str_starts_with( $name_low, 'data-' ) && ! empty( $allowed_attr['data-*'] )
1363-
&& preg_match( '/^data-[a-z0-9_-]+$/', $name_low, $match )
1364-
) {
1365+
$dataset_name = wp_js_dataset_name( $name );
1366+
if ( isset( $dataset_name ) && ! empty( $allowed_attr['data-*'] ) ) {
13651367
/*
1366-
* Add the whole attribute name to the allowed attributes and set any restrictions
1367-
* for the `data-*` attribute values for the current element.
1368+
* The attribute name passed in here is the `data-*` name, or the name in
1369+
* the raw HTML. Add it to the set of allowed attributes and adopt the
1370+
* restrictions applied to all custom data attributes for the element.
13681371
*/
1369-
$allowed_attr[ $match[0] ] = $allowed_attr['data-*'];
1372+
$allowed_attr[ $name_low ] = $allowed_attr['data-*'];
13701373
} else {
13711374
$name = '';
13721375
$value = '';

tests/phpunit/tests/kses.php

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,25 +1441,114 @@ public function data_safecss_filter_attr() {
14411441
/**
14421442
* Data attributes are globally accepted.
14431443
*
1444-
* @ticket 33121
1444+
* @ticket 61501
1445+
*
1446+
* @dataProvider data_data_attributes_and_whether_they_are_allowed
1447+
*
1448+
* @param string $attribute_name Custom data attribute, e.g. "data-wp-bind--enabled".
1449+
* @param bool $is_allowed Whether the given attribute should be allowed.
14451450
*/
1446-
public function test_wp_kses_attr_data_attribute_is_allowed() {
1447-
$test = '<div data-foo="foo" data-bar="bar" datainvalid="gone" data-two-hyphens="remains">Pens and pencils</div>';
1448-
$expected = '<div data-foo="foo" data-bar="bar" data-two-hyphens="remains">Pens and pencils</div>';
1451+
public function test_wp_kses_attr_boolean_data_attribute_is_allowed( string $attribute_name, bool $is_allowed ) {
1452+
$element = "<div {$attribute_name}>Pens and pencils.</div>";
14491453

1450-
$this->assertEqualHTML( $expected, wp_kses_post( $test ) );
1454+
$processor = new WP_HTML_Tag_Processor( $element );
1455+
$processor->next_tag();
1456+
1457+
$this->assertTrue(
1458+
$processor->get_attribute( $attribute_name ),
1459+
"Failed to find expected attribute '{$attribute_name}' before filtering: check test."
1460+
);
1461+
1462+
$processor = new WP_HTML_Tag_Processor( wp_kses_post( $element ) );
1463+
$this->assertTrue(
1464+
$processor->next_tag(),
1465+
'Failed to find containing tag after filtering: check test.'
1466+
);
1467+
1468+
if ( $is_allowed ) {
1469+
$this->assertTrue(
1470+
$processor->get_attribute( $attribute_name ),
1471+
"Allowed custom data attribute '{$attribute_name}' should not have been removed."
1472+
);
1473+
} else {
1474+
$this->assertNull(
1475+
$processor->get_attribute( $attribute_name ),
1476+
"Should have removed un-allowed custom data attribute '{$attribute_name}'."
1477+
);
1478+
}
14511479
}
14521480

14531481
/**
1454-
* Data attributes with leading, trailing, and double "-" are globally accepted.
1482+
* Ensures that only allowable custom data attributes with values are retained.
1483+
*
1484+
* @ticket 33121
1485+
*
1486+
* @dataProvider data_data_attributes_and_whether_they_are_allowed
14551487
*
1456-
* @ticket 61052
1488+
* @param string $attribute_name Custom data attribute, e.g. "dat-wp-bind--enabled".
1489+
* @param bool $is_allowed Whether the given attribute should be allowed.
14571490
*/
1458-
public function test_wp_kses_attr_data_attribute_hypens_allowed() {
1459-
$test = '<div data--leading="remains" data-trailing-="remains" data-middle--double="remains">Pens and pencils</div>';
1460-
$expected = '<div data--leading="remains" data-trailing-="remains" data-middle--double="remains">Pens and pencils</div>';
1491+
public function test_wp_kses_attr_data_attribute_is_allowed( string $attribute_name, bool $is_allowed ) {
1492+
$element = "<div {$attribute_name}='shadows and dust'>Pens and pencils.</div>";
14611493

1462-
$this->assertEqualHTML( $expected, wp_kses_post( $test ) );
1494+
$processor = new WP_HTML_Tag_Processor( $element );
1495+
$processor->next_tag();
1496+
1497+
$this->assertIsString(
1498+
$processor->get_attribute( $attribute_name ),
1499+
"Failed to find expected attribute '{$attribute_name}' before filtering: check test."
1500+
);
1501+
1502+
$processor = new WP_HTML_Tag_Processor( wp_kses_post( $element ) );
1503+
$this->assertTrue(
1504+
$processor->next_tag(),
1505+
'Failed to find containing tag after filtering: check test.'
1506+
);
1507+
1508+
if ( $is_allowed ) {
1509+
$this->assertIsString(
1510+
$processor->get_attribute( $attribute_name ),
1511+
"Allowed custom data attribute '{$attribute_name}' should not have been removed."
1512+
);
1513+
} else {
1514+
$this->assertNull(
1515+
$processor->get_attribute( $attribute_name ),
1516+
"Should have removed un-allowed custom data attribute '{$attribute_name}'."
1517+
);
1518+
}
1519+
}
1520+
1521+
/**
1522+
* Data provider.
1523+
*
1524+
* @return array[].
1525+
*/
1526+
public static function data_data_attributes_and_whether_they_are_allowed() {
1527+
return array(
1528+
// Allowable custom data attributes.
1529+
'Normative attribute' => array( 'data-foo', true ),
1530+
'Non-consecutive dashes' => array( 'data-two-hyphens', true ),
1531+
'Double-dashes' => array( 'data--double-dash', true ),
1532+
'Trailing dash' => array( 'data-trailing-dash-', true ),
1533+
'Uppercase alphas' => array( 'data-Post-ID', true ),
1534+
'Bind Directive' => array( 'data-wp-bind--enabled', true ),
1535+
'Single-dash suffix' => array( 'data-after-', true ),
1536+
'Double-dash prefix' => array( 'data--before', true ),
1537+
'Double-dash suffix' => array( 'data-after--', true ),
1538+
'Double-dashes everywhere' => array( 'data--one--two--', true ),
1539+
'Underscore' => array( 'data-over_under', true ),
1540+
1541+
// Not custom data attributes.
1542+
'No data- prefix' => array( 'post-id', false ),
1543+
'No dash after prefix' => array( 'datainvalid', false ),
1544+
1545+
// Un-allowable custom data attributes.
1546+
'Nothing after prefix' => array( 'data-', false ),
1547+
'Whitespace after prefix' => array( "data-\u{2003}", false ),
1548+
'Emoji in name' => array( 'data-🐄', false ),
1549+
'Brackets' => array( 'data-[enabled]', false ),
1550+
'Colon' => array( 'data-wp:bind', false ),
1551+
);
14631552
}
14641553

14651554
/**

0 commit comments

Comments
 (0)