diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 74a28109b6536..7a980c5f56790 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -4790,6 +4790,30 @@ function esc_attr( $text ) { return apply_filters( 'attribute_escape', $safe_text, $text ); } +/** + * Escaping for HTML attribute names. + * + * @since 7.1.0 + * + * @param string $text The attribute name to escape. + * @return string The escaped attribute name. + */ +function esc_attr_name( $text ) { + $safe_text = wp_check_invalid_utf8( $text ); + + $safe_text = preg_replace( '/[^a-zA-Z0-9_.:\[\]-]+/u', '', $safe_text ); + + /** + * Filters a string cleaned and escaped for output as an HTML attribute name. + * + * @since 7.1.0 + * + * @param string $safe_text The attribute name after it has been escaped. + * @param string $text The attribute name prior to being escaped. + */ + return apply_filters( 'esc_attr_name', $safe_text, $text ); +} + /** * Escaping for textarea values. * diff --git a/tests/phpunit/tests/formatting/escAttrName.php b/tests/phpunit/tests/formatting/escAttrName.php new file mode 100644 index 0000000000000..ce13df6b1e55d --- /dev/null +++ b/tests/phpunit/tests/formatting/escAttrName.php @@ -0,0 +1,90 @@ +assertSame( $attr, esc_attr_name( $attr ) ); + } + + /** + * @return array[] + */ + public function data_valid_attribute_names() { + return array( + array( 'class' ), + array( 'data-my-value' ), + array( 'aria-label' ), + array( 'my_attr' ), + array( 'attr123' ), + array( 'name[key]' ), + array( 'xml:lang' ), + array( 'x-my.attr' ), + array( 'MyAttr' ), + array( 'data-foo_bar.baz[0]' ), + ); + } + + /** + * @dataProvider data_forbidden_chars + * + * @param string $input Attribute name containing forbidden characters. + * @param string $expected Expected output after escaping. + */ + public function test_forbidden_chars_are_removed( $input, $expected ) { + $this->assertSame( $expected, esc_attr_name( $input ) ); + } + + /** + * @return array[] + */ + public function data_forbidden_chars() { + return array( + array( 'foo bar', 'foobar' ), + array( '"data-foo"', 'data-foo' ), + array( "'data-foo'", 'data-foo' ), + array( 'foo>bar', 'foobar' ), + array( 'foo<=/', '' ), + array( 'data-' . "\xc0\x80" . 'foo', '' ), + array( 'data-ñame', 'data-ame' ), + array( 'data-😀', 'data-' ), + array( 'attrф', 'attr' ), + ); + } + + public function test_filter_is_applied() { + add_filter( 'esc_attr_name', array( $this, 'filter_attr_name' ), 10, 2 ); + + $result = esc_attr_name( 'data-foo' ); + + remove_filter( 'esc_attr_name', array( $this, 'filter_attr_name' ) ); + + $this->assertSame( 'filtered-data-foo', $result ); + } + + public function filter_attr_name( $safe_text, $text ) { + return 'filtered-' . $safe_text; + } +}