Repository navigation
Conversation
|
Hi @ShaneMuir! 👋 Thank you for your contribution to WordPress! 💖 It looks like this is your first pull request to No one monitors this repository for new pull requests. Pull requests must be attached to a Trac ticket to be considered for inclusion in WordPress Core. To attach a pull request to a Trac ticket, please include the ticket's full URL in your pull request description. Pull requests are never merged on GitHub. The WordPress codebase continues to be managed through the SVN repository that this GitHub repository mirrors. Please feel free to open pull requests to work on any contribution you are making. More information about how GitHub pull requests can be used to contribute to WordPress can be found in the Core Handbook. Please include automated tests. Including tests in your pull request is one way to help your patch be considered faster. To learn about WordPress' test suites, visit the Automated Testing page in the handbook. If you have not had a chance, please review the Contribute with Code page in the WordPress Core Handbook. The Developer Hub also documents the various coding standards that are followed:
Thank you, |
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @shane.muirhead@tribusdigital.com. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
| ( str_starts_with( $content, 'https://' ) && in_array( 'https', $allowed_protocols, true ) ) || | ||
| ( str_starts_with( $content, 'http://' ) && in_array( 'http', $allowed_protocols, true ) ) | ||
| ( str_starts_with( $content, 'http://' ) && in_array( 'http', $allowed_protocols, true ) ) || | ||
| ( str_starts_with( $content, '#:~:text' ) ) |
There was a problem hiding this comment.
This will make URLs starting with a text fragment impossible to disable. Is there any specific reason for that? If not, thinking the last line should be replaced with:
( str_starts_with( $content, '#:~:text' ) && in_array( '#:~:text', $allowed_protocols, true ) )
Then the #:~:text string should be added to the default $allowed_protocols.
| ( str_starts_with( $content, 'https://' ) && in_array( 'https', $allowed_protocols, true ) ) || | ||
| ( str_starts_with( $content, 'http://' ) && in_array( 'http', $allowed_protocols, true ) ) | ||
| ( str_starts_with( $content, 'http://' ) && in_array( 'http', $allowed_protocols, true ) ) || | ||
| ( str_starts_with( $content, '#:~:text' ) ) |
There was a problem hiding this comment.
And a nitpick :)
Looking at the specs, should the identifier string be #:~:text or just #:~:?
There was a problem hiding this comment.
I think you are correct here it could just be fragment directive I will make amends.
There was a problem hiding this comment.
Thank you @azaozz for reviewing. I've made some amends based on your recommendations, added check so that the protocol can be disabled through removing the protocol from the default array. Removed the 'text' part of the directive string and added a test to check fragment directive is removed if removed from default protocol array.
|
This patch looks generally okay. Left couple questions for possible changes/improvements. Looking at the specs: https://wicg.github.io/scroll-to-text-fragment/#syntax and https://wicg.github.io/scroll-to-text-fragment/#the-fragment-directive it seems there aren't any security implications. URLs starting with |
…otocol() to ensure it checks for the fragment directive in allowed protocols. Added extra units tests to check fragment directive is removed if not in allowed protocols.
…ix/kses-allow-text-fragments
…ments. Remove the text fragment from since it was advised these aren't protocols. Updated unit tests
|
Updated PR on Ticket: #60347 |
| if ( is_allowed_text_fragment( $content ) ) { | ||
| return $content; | ||
| } |
There was a problem hiding this comment.
I don't see the need for introducing a new function for this. I think it should be just:
| if ( is_allowed_text_fragment( $content ) ) { | |
| return $content; | |
| } | |
| if ( str_starts_with( $content, '#:~:text=' ) ) { | |
| return $content; | |
| } |
However, for completeness, should it also take into account $allowed_protocols? What if you called wp_kses_bad_protocol( $content, array( 'mailto' ) )? This should get rightfully rejected.
|
|
||
| /** | ||
| * Checks if a content string is a valid text fragment link. | ||
| * | ||
| * This function handles the text fragment links (e.g., #:~:text=highlight) separately. | ||
| * | ||
| * @param string $content | ||
| * @return bool | ||
| */ | ||
| function is_allowed_text_fragment( string $content ): bool { | ||
| return str_starts_with( $content, '#:~:text=' ); | ||
| } |
There was a problem hiding this comment.
| /** | |
| * Checks if a content string is a valid text fragment link. | |
| * | |
| * This function handles the text fragment links (e.g., #:~:text=highlight) separately. | |
| * | |
| * @param string $content | |
| * @return bool | |
| */ | |
| function is_allowed_text_fragment( string $content ): bool { | |
| return str_starts_with( $content, '#:~:text=' ); | |
| } |
| /** | ||
| * Test to ensure wp_kses() allows text fragment links. | ||
| * | ||
| * This test verifies that wp_kses() correctly handles and preserves | ||
| * text fragment links (e.g., #:~:text=highlight) in the href attribute. | ||
| * | ||
| * @ticket 60347 | ||
| * | ||
| * @return void | ||
| */ | ||
| public function test_wp_kses_allows_text_fragments() { | ||
| $html = '<a href="#:~:text=highlight">Text Fragment</a>'; | ||
| $allowed_html = array( | ||
| 'a' => array( | ||
| 'href' => true, | ||
| ), | ||
| ); | ||
|
|
||
| $result = wp_kses( $html, $allowed_html ); | ||
| $this->assertSame( $html, $result ); | ||
| } |
There was a problem hiding this comment.
Let's refactor this to use a data provider. The data provider should include cases for:
#:~:text=highlight/absolute/path/#:~:text=highlighthttps://example.com/#:~:text=highlight
Maybe others.
There was a problem hiding this comment.
Also, it should check for non-text fragment anchor links as well, like #footer.
| $this->assertNotSame( $html, $result ); | ||
| $this->assertStringNotContainsString( 'javascript:', $result ); |
There was a problem hiding this comment.
Let's use assertEqualHTML here.
| ); | ||
|
|
||
| $result = wp_kses( $html, $allowed_html ); | ||
| $this->assertSame( $html, $result ); |
There was a problem hiding this comment.
Using assertSame works, but assertEqualHTML is probably better.
|
Updated the PR to address @westonruter’s review comments: 3b3ad66 |
dhrupo
left a comment
There was a problem hiding this comment.
Tested 374f911. The earlier review feedback looks addressed: data provider, assertEqualHTML, the #footer case, and honouring $allowed_protocols.
Results (PHP 7.4.33, MySQL 8.4.5):
--group 60347: 16/16 pass. Run against trunk'skses.php, 7 of them fail, so the tests cover the bug.--group kses: 486 tests pass.- PHPCS (
--warning-severity=1) on both files: clean. javascript:,javascript:,javascript:,javascript:and:(with and without the semicolon), a tab inside the scheme, anddata:are all still stripped when followed by#:~:text=.
One gap: a relative URL with more than one query parameter is still broken, because [^:&]* stops at the first &:
/page?a=1&b=2#:~:text=foo → href="text=foo"
?p=1&lang=en#:~:text=foo → href="text=foo"
Excluding & is what keeps encoded colons out, so the suggestion below only excludes the & sequences that can produce a colon (&#… numeric references and :). With it, both URLs above are preserved, every encoded-scheme case above is still stripped, and the existing 16 tests and the kses group still pass.
Possible extra data provider cases:
'relative URL with query string' => array(
'/page?a=1&b=2#:~:text=highlight',
'/page?a=1&b=2#:~:text=highlight',
array( 'http', 'https' ),
),
'named entity colon before fragment' => array(
'javascript:alert(1)#:~:text=highlight',
'text=highlight',
array( 'http', 'https' ),
),
'hex entity colon before fragment' => array(
'javascript:alert(1)#:~:text=highlight',
'text=highlight',
array( 'http', 'https' ),
),Not for this PR, but related: a fragment containing a colon that isn't a text directive, such as #section:2 or /page#section:2, still comes out as 2 (same on trunk). That seems worth its own ticket.
AI disclosure: I used Claude Code to help write the probe cases and draft this review. I ran and checked every result above myself.
| // Preserve relative text fragment URLs when web protocols are allowed. | ||
| if ( | ||
| ( in_array( 'http', $allowed_protocols, true ) || in_array( 'https', $allowed_protocols, true ) ) && | ||
| preg_match( '/^[^:&]*#:~:text=/', $content ) |
There was a problem hiding this comment.
Allows & (e.g. a query string) before the fragment while still rejecting &#… and :, which can decode to a colon:
| preg_match( '/^[^:&]*#:~:text=/', $content ) | |
| preg_match( '/^(?:[^:&]|&(?!#|colon;))*#:~:text=/i', $content ) |
This PR addresses the issue in Trac ticket #60347, where wp_kses() removes valid text fragment links (e.g., #:~:text=highlight) by treating them as invalid protocols.
Changes:
Testing:
Impact:
This change ensures that text fragment links are correctly preserved by wp_kses(), aligning with modern URL standards and avoiding unnecessary sanitization.
Trac ticket: https://core.trac.wordpress.org/ticket/60347
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.