diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 0b24a1ba0c..3a7f99dd1a 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -271,6 +271,13 @@ if ( ! CUSTOM_TAGS ) { 'lang' => true, 'xml:lang' => true, ), + 'object' => array( + 'data' => true, + 'type' => array( + 'required' => true, + 'values' => array( 'application/pdf' ), + ), + ), 'p' => array( 'align' => true, 'dir' => true, @@ -1165,15 +1172,47 @@ function wp_kses_attr( $element, $attr, $allowed_html, $allowed_protocols ) { // Split it. $attrarr = wp_kses_hair( $attr, $allowed_protocols ); + // Check if there are attributes that are required. + $required_attrs = array_filter( + $allowed_html[ $element_low ], + function( $required_attr_limits ) { + return isset( $required_attr_limits['required'] ) && true === $required_attr_limits['required']; + } + ); + + // If a required attribute check fails, we can return nothing for a self-closing tag, + // but for a non-self-closing tag the best option is to return the element with attributes, + // as KSES doesn't handle matching the relevant closing tag. + $stripped_tag = ''; + if ( empty( $xhtml_slash ) ) { + $stripped_tag = "<$element>"; + } + // Go through $attrarr, and save the allowed attributes for this element // in $attr2. $attr2 = ''; foreach ( $attrarr as $arreach ) { + // Check if this attribute is required. + $required = isset( $required_attrs[ strtolower( $arreach['name'] ) ] ); + if ( wp_kses_attr_check( $arreach['name'], $arreach['value'], $arreach['whole'], $arreach['vless'], $element, $allowed_html ) ) { $attr2 .= ' ' . $arreach['whole']; + + // If this was a required attribute, we can mark it as found. + if ( $required ) { + unset( $required_attrs[ strtolower( $arreach['name'] ) ] ); + } + } elseif ( $required ) { + // This attribute was required, but didn't pass the check. The entire tag is not allowed. + return $stripped_tag; } } + // If some required attributes weren't set, the entire tag is not allowed. + if ( ! empty( $required_attrs ) ) { + return $stripped_tag; + } + // Remove any "<" or ">" characters. $attr2 = preg_replace( '/[<>]/', '', $attr2 ); @@ -1600,6 +1639,17 @@ function wp_kses_check_attr_val( $value, $vless, $checkname, $checkvalue ) { $ok = false; } break; + + case 'values': + /* + * The values check is used when you want to make sure that the attribute + * has one of the given values. + */ + + if ( false === array_search( strtolower( $value ), $checkvalue, true ) ) { + $ok = false; + } + break; } // End switch. return $ok; diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php index 961ed71b16..11434d37e8 100644 --- a/tests/phpunit/tests/kses.php +++ b/tests/phpunit/tests/kses.php @@ -1496,4 +1496,298 @@ EOF; $this->assertSame( $html, wp_kses_post( $html ) ); } + + /** + * Test that object tags are allowed under limited circumstances. + * + * @ticket 54261 + * + * @dataProvider data_wp_kses_object_tag_allowed + * + * @param string $html A string of HTML to test. + * @param string $expected The expected result from KSES. + */ + function test_wp_kses_object_tag_allowed( $html, $expected ) { + $this->assertSame( $expected, wp_kses_post( $html ) ); + } + + /** + * Data provider for test_wp_kses_object_tag_allowed(). + */ + function data_wp_kses_object_tag_allowed() { + return array( + 'valid value for type' => array( + '', + '', + ), + 'invalid value for type' => array( + '', + '', + ), + 'multiple type attributes, last invalid' => array( + '', + '', + ), + 'multiple type attributes, first uppercase, last invalid' => array( + '', + '', + ), + 'multiple type attributes, last upper case and invalid' => array( + '', + '', + ), + 'multiple type attributes, first invalid' => array( + '', + '', + ), + 'multiple type attributes, first upper case and invalid' => array( + '', + '', + ), + 'multiple type attributes, first invalid, last uppercase' => array( + '', + '', + ), + 'multiple object tags, last invalid' => array( + '', + '', + ), + 'multiple object tags, first invalid' => array( + '', + '', + ), + 'type attribute with partially incorrect value' => array( + '', + '', + ), + 'type attribute with empty value' => array( + '', + '', + ), + 'type attribute with no value' => array( + '', + '', + ), + 'no type attribute' => array( + '', + '', + ), + ); + } + + /** + * Test that object tags will continue to function if they've been added using the + * 'wp_kses_allowed_html' filter. + * + * @ticket 54261 + */ + function test_wp_kses_object_added_in_html_filter() { + $html = << + + + +HTML; + + add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_object_added_in_html_filter' ), 10, 2 ); + + $filtered_html = wp_kses_post( $html ); + + remove_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_object_added_in_html_filter' ) ); + + $this->assertSame( $html, $filtered_html ); + } + + function filter_wp_kses_object_added_in_html_filter( $tags, $context ) { + if ( 'post' === $context ) { + $tags['object'] = array( + 'type' => true, + 'data' => true, + ); + + $tags['param'] = array( + 'name' => true, + 'value' => true, + ); + } + + return $tags; + } + + /** + * Test that attributes with a list of allowed values are filtered correctly. + * + * @ticket 54261 + * + * @dataProvider data_wp_kses_allowed_values_list + * + * @param string $html A string of HTML to test. + * @param string $expected The expected result from KSES. + * @param array $allowed_html The allowed HTML to pass to KSES. + */ + function test_wp_kses_allowed_values_list( $html, $expected, $allowed_html ) { + $this->assertSame( $expected, wp_kses( $html, $allowed_html ) ); + } + + /** + * Data provider for test_wp_kses_allowed_values_list(). + */ + function data_wp_kses_allowed_values_list() { + $data = array( + 'valid dir attribute value' => array( + '

foo

', + '

foo

', + ), + 'valid dir attribute value, upper case' => array( + '

foo

', + '

foo

', + ), + 'invalid dir attribute value' => array( + '

foo

', + '

foo

', + ), + 'dir attribute with empty value' => array( + '

foo

', + '

foo

', + ), + 'dir attribute with no value' => array( + '

foo

', + '

foo

', + ), + ); + + return array_map( + function ( $datum ) { + $datum[] = array( + 'p' => array( + 'dir' => array( + 'values' => array( 'ltr', 'rtl' ), + ), + ), + ); + + return $datum; + }, + $data + ); + } + + /** + * Test that attributes with the required flag are handled correctly. + * + * @ticket 54261 + * + * @dataProvider data_wp_kses_required_attribute + * + * @param string $html A string of HTML to test. + * @param string $expected The expected result from KSES. + * @param array $allowed_html The allowed HTML to pass to KSES. + */ + function test_wp_kses_required_attribute( $html, $expected, $allowed_html ) { + $this->assertSame( $expected, wp_kses( $html, $allowed_html ) ); + } + + /** + * Data provider for test_wp_kses_required_attribute(). + */ + function data_wp_kses_required_attribute() { + $data = array( + 'valid dir attribute value' => array( + '

foo

', // Test HTML. + '

foo

', // Expected result when dir is not required. + '

foo

', // Expected result when dir is required. + '

foo

', // Expected result when dir is required, but has no value filter. + ), + 'valid dir attribute value, upper case' => array( + '

foo

', + '

foo

', + '

foo

', + '

foo

', + ), + 'invalid dir attribute value' => array( + '

foo

', + '

foo

', + '

foo

', + '

foo

', + ), + 'dir attribute with empty value' => array( + '

foo

', + '

foo

', + '

foo

', + '

foo

', + ), + 'dir attribute with no value' => array( + '

foo

', + '

foo

', + '

foo

', + '

foo

', + ), + 'dir attribute not set' => array( + '

foo

', + '

foo

', + '

foo

', + '

foo

', + ), + ); + + $return_data = array(); + + foreach ( $data as $description => $datum ) { + // Test that the required flag defaults to false. + $return_data[ "$description - required flag not set" ] = array( + $datum[0], + $datum[1], + array( + 'p' => array( + 'dir' => array( + 'values' => array( 'ltr', 'rtl' ), + ), + ), + ), + ); + + // Test when the attribute is not required, but has allowed values. + $return_data[ "$description - required flag set to false" ] = array( + $datum[0], + $datum[1], + array( + 'p' => array( + 'dir' => array( + 'required' => false, + 'values' => array( 'ltr', 'rtl' ), + ), + ), + ), + ); + + // Test when the attribute is required, but has allowed values. + $return_data[ "$description - required flag set to true" ] = array( + $datum[0], + $datum[2], + array( + 'p' => array( + 'dir' => array( + 'required' => true, + 'values' => array( 'ltr', 'rtl' ), + ), + ), + ), + ); + + // Test when the attribute is required, but has no allowed values. + $return_data[ "$description - required flag set to true, no allowed values specified" ] = array( + $datum[0], + $datum[3], + array( + 'p' => array( + 'dir' => array( + 'required' => true, + ), + ), + ), + ); + } + + return $return_data; + } }