Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/wp-includes/html-api/class-wp-html-decoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,25 @@ public static function attribute_starts_with( $haystack, $search_text, $case_sen
return false;
}

// If there's no character reference but the character do match, then it could still match.
// If there's no character reference but the characters do match, then it could still match.
if ( null === $next_chunk && $chars_match ) {
++$haystack_at;
++$search_at;
continue;
}

// If there is a character reference, then the decoded value must exactly match what follows in the search string.
if ( 0 !== substr_compare( $search_text, $next_chunk, $search_at, strlen( $next_chunk ), $loose_case ) ) {
$match_length = min( strlen( $next_chunk ), $search_length - $search_at );
if ( 0 !== substr_compare( $search_text, $next_chunk, $search_at, $match_length, $loose_case ) ) {
return false;
}

// The character reference matched, so continue checking.
$haystack_at += $token_length;
$search_at += strlen( $next_chunk );
$search_at += $match_length;
}

return true;
return $search_at === $search_length;
}

/**
Expand Down
68 changes: 68 additions & 0 deletions tests/phpunit/tests/html-api/wpHtmlDecoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,74 @@ public static function data_case_variants_of_attribute_prefixes() {
}
}

/**
* Ensures that `attribute_starts_with` checks the full search string.
*
* @ticket 65372
*
* @dataProvider data_attribute_starts_with_search_string_boundaries
*
* @param string $attribute_value Raw attribute value from HTML string.
* @param string $search_string Prefix contained or not contained in encoded attribute value.
* @param bool $is_match Whether the search string is a prefix for the attribute value.
*/
public function test_attribute_starts_with_checks_search_string_boundaries(
string $attribute_value,
string $search_string,
bool $is_match
): void {
if ( $is_match ) {
$this->assertTrue(
WP_HTML_Decoder::attribute_starts_with( $attribute_value, $search_string, 'case-sensitive' ),
'Should have matched attribute prefix.'
);
} else {
$this->assertFalse(
WP_HTML_Decoder::attribute_starts_with( $attribute_value, $search_string, 'case-sensitive' ),
'Should not have matched attribute with prefix.'
);
}
}

/**
* Data provider.
*
* @return Generator<string, array{string, string, string, bool}> Test cases.
*/
public static function data_attribute_starts_with_search_string_boundaries(): Generator {
yield 'Empty attribute does not match non-empty prefix' => array( '', 'http', false );
yield 'Short attribute does not match longer prefix' => array(
'java',
'javascript',
false,
);
yield 'Longer attribute matches shorter prefix' => array(
'javascript',
'java',
true,
);
yield "&fjlig; (decodes to 2-byte 'fj') starts with f" => array(
'&fjlig; is literally "f" followed by "j"',
'f',
true,
);
yield "&nvlt; (decodes to 2-byte '<⃒') starts with '<'" => array(
'&nvlt;script>',
'<',
true,
);
yield "Combining character references (¬̸) full match on '¬̸' prefix" => array(
'&not;&#x0338; A negated not?',
'¬̸',
true,
);
yield "Combining character references (¬̸) partial match on '¬' prefix" => array(
'&not;&$#x0338; A negated not?',
'¬',
true,
);
}

/**
* Ensures that `attribute_starts_with` respects the case sensitivity argument.
*
Expand Down
Loading