diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 2f23f0e8781e1..b0759ae1cf1cc 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -4709,11 +4709,28 @@ function paginate_links( $args = '' ) { if ( $args['prev_next'] && $current && 1 < $current ) : $link = str_replace( '%_%', 2 === $current ? '' : $args['format'], $args['base'] ); $link = str_replace( '%#%', $current - 1, $link ); + /* + * Maybe add a trailing slash to the link according to the site's settings. + * + * Only add this for links without a query string or a fragment. Links with + * these components will have data changed if the trailing slash is added. + * + * This only affects sites with pretty permalinks, as sites without them + * enabled will include a query string parameter. + * + * For links to the base of a domain, a trailing slash is always added to + * the link as that's how browsers handle URLs. + */ + if ( in_array( wp_parse_url( $link, PHP_URL_PATH ), array( null, '/' ), true ) && ! str_contains( $link, '?' ) && ( ! str_contains( $link, '#' ) ) ) { + $link = trailingslashit( $link ); + } elseif ( ! str_contains( $link, '?' ) && ( ! str_contains( $link, '#' ) ) ) { + $link = user_trailingslashit( $link, 'paged' ); + } + if ( $add_args ) { $link = add_query_arg( $add_args, $link ); } $link .= $args['add_fragment']; - $link = get_option( 'permalink_structure' ) ? user_trailingslashit( $link, 'paged' ) : $link; $page_links[] = sprintf( '', @@ -4742,11 +4759,27 @@ function paginate_links( $args = '' ) { if ( $args['show_all'] || ( $n <= $end_size || ( $current && $n >= $current - $mid_size && $n <= $current + $mid_size ) || $n > $total - $end_size ) ) : $link = str_replace( '%_%', 1 === $n ? '' : $args['format'], $args['base'] ); $link = str_replace( '%#%', $n, $link ); + /* + * Maybe add a trailing slash to the link according to the site's settings. + * + * Only add this for links without a query string or a fragment. Links with + * these components will have data changed if the trailing slash is added. + * + * This only affects sites with pretty permalinks, as sites without them + * enabled will include a query string parameter. + * + * For links to the base of a domain, a trailing slash is always added to + * the link as that's how browsers handle URLs. + */ + if ( in_array( wp_parse_url( $link, PHP_URL_PATH ), array( null, '/' ), true ) && ! str_contains( $link, '?' ) && ( ! str_contains( $link, '#' ) ) ) { + $link = trailingslashit( $link ); + } elseif ( ! str_contains( $link, '?' ) && ( ! str_contains( $link, '#' ) ) ) { + $link = user_trailingslashit( $link, 'paged' ); + } if ( $add_args ) { $link = add_query_arg( $add_args, $link ); } $link .= $args['add_fragment']; - $link = get_option( 'permalink_structure' ) ? user_trailingslashit( $link, 'paged' ) : $link; $page_links[] = sprintf( '%s', @@ -4767,11 +4800,27 @@ function paginate_links( $args = '' ) { if ( $args['prev_next'] && $current && $current < $total ) : $link = str_replace( '%_%', $args['format'], $args['base'] ); $link = str_replace( '%#%', $current + 1, $link ); + /* + * Maybe add a trailing slash to the link according to the site's settings. + * + * Only add this for links without a query string or a fragment. Links with + * these components will have data changed if the trailing slash is added. + * + * This only affects sites with pretty permalinks, as sites without them + * enabled will include a query string parameter. + * + * For links to the base of a domain, a trailing slash is always added to + * the link as that's how browsers handle URLs. + */ + if ( in_array( wp_parse_url( $link, PHP_URL_PATH ), array( null, '/' ), true ) && ! str_contains( $link, '?' ) && ( ! str_contains( $link, '#' ) ) ) { + $link = trailingslashit( $link ); + } elseif ( ! str_contains( $link, '?' ) && ( ! str_contains( $link, '#' ) ) ) { + $link = user_trailingslashit( $link, 'paged' ); + } if ( $add_args ) { $link = add_query_arg( $add_args, $link ); } $link .= $args['add_fragment']; - $link = get_option( 'permalink_structure' ) ? user_trailingslashit( $link, 'paged' ) : $link; $page_links[] = sprintf( '', diff --git a/tests/phpunit/tests/general/paginateLinks.php b/tests/phpunit/tests/general/paginateLinks.php index 0d1057c919e55..575dd04bd2d00 100644 --- a/tests/phpunit/tests/general/paginateLinks.php +++ b/tests/phpunit/tests/general/paginateLinks.php @@ -438,4 +438,211 @@ public function test_pagination_links_without_trailing_slash() { 'Previous link should not have trailing slash when permalink structure has no trailing slash' ); } + + /** + * @ticket 61393 + * @ticket 63123 + * + * @dataProvider data_search_terms + * + * @param string $search_term Search term - should only be the value passed to `s` parameter. + * @param string $not_expected Search term that is unexpected - should only be the value unexpected in the `s` parameter. + */ + public function test_pagination_links_does_not_modify_search_terms_with_trailing_slash_permalinks( $search_term, $not_expected ) { + $this->set_permalink_structure( '/%postname%/' ); + + $args = array( + 'base' => 'http://example.org/%_%', + 'format' => 'page/%#%', + 'total' => 5, + 'current' => 3, + 'prev_next' => true, + 'add_args' => array( + 's' => $search_term, + ), + ); + + $links = paginate_links( $args ); + + $this->assertStringNotContainsString( + "?s={$not_expected}\"", + $links, + 'Search term should not be modified.' + ); + } + + /** + * @ticket 61393 + * @ticket 63123 + * + * @dataProvider data_search_terms + * + * @param string $search_term Search term - should only be the value passed to `s` parameter. + * @param string $not_expected Search term that is unexpected - should only be the value unexpected in the `s` parameter. + */ + public function test_pagination_links_does_not_modify_search_terms_in_base_with_trailing_slash_permalinks( $search_term, $not_expected ) { + $this->set_permalink_structure( '/%postname%/' ); + + $args = array( + 'base' => "http://example.org/%_%/?s={$search_term}", + 'format' => 'page/%#%', + 'total' => 5, + 'current' => 3, + 'prev_next' => true, + 'add_args' => array( + 's' => $search_term, + ), + ); + + $links = paginate_links( $args ); + + $this->assertStringNotContainsString( + "?s={$not_expected}\"", + $links, + 'Search term should not be modified.' + ); + } + + /** + * @ticket 61393 + * @ticket 63123 + * + * @dataProvider data_search_terms + * + * @param string $search_term Search term - should only be the value passed to `s` parameter. + * @param string $not_expected Search term that is unexpected - should only be the value unexpected in the `s` parameter. + */ + public function test_pagination_links_does_not_modify_search_terms_without_trailing_slash_permalinks( $search_term, $not_expected ) { + $this->set_permalink_structure( '/%postname%' ); + + $args = array( + 'base' => "http://example.org/%_%?s={$search_term}", + 'format' => 'page/%#%', + 'total' => 5, + 'current' => 3, + 'prev_next' => true, + ); + + $links = paginate_links( $args ); + + $this->assertStringNotContainsString( + "?s={$not_expected}\"", + $links, + 'Search term should not be modified.' + ); + } + + /** + * @ticket 61393 + * @ticket 63123 + * + * @dataProvider data_search_terms + * + * @param string $search_term Search term - should only be the value passed to `s` parameter. + * @param string $not_expected Search term that is unexpected - should only be the value unexpected in the `s` parameter. + */ + public function test_pagination_links_does_not_modify_search_terms_in_base_without_trailing_slash_permalinks( $search_term, $not_expected ) { + $this->set_permalink_structure( '/%postname%' ); + + $args = array( + 'base' => 'http://example.org/%_%', + 'format' => 'page/%#%', + 'total' => 5, + 'current' => 3, + 'prev_next' => true, + 'add_args' => array( + 's' => $search_term, + ), + ); + + $links = paginate_links( $args ); + + $this->assertStringNotContainsString( + "?s={$not_expected}\"", + $links, + 'Search term should not be modified.' + ); + } + + /** + * Data provider for test_pagination_links_does_not_modify_search_terms_* tests. + * + * @return array[] Data provider. + */ + public function data_search_terms() { + return array( + 'search term without trailing slash' => array( 'search+term', 'search+term/' ), + 'search term with trailing slash' => array( 'search+term/', 'search+term' ), + ); + } + + /** + * @ticket 61393 + * @ticket 63123 + * + * @dataProvider data_url_fragments + * + * @param string $url_fragment URL fragment - should only be the value following the `#`. + * @param string $not_expected URL fragment that is unexpected - should only be the value unexpected following the `#`. + */ + public function test_pagination_links_does_not_modify_url_fragments_with_trailing_slash_permalinks( $url_fragment, $not_expected ) { + $this->set_permalink_structure( '/%postname%/' ); + + $args = array( + 'base' => "http://example.org/%_%#{$url_fragment}", + 'format' => 'page/%#%', + 'total' => 5, + 'current' => 3, + 'prev_next' => true, + ); + + $links = paginate_links( $args ); + + $this->assertStringNotContainsString( + "#{$not_expected}\"", + $links, + 'URL fragments should not be modified to include a trailing slash.' + ); + } + + /** + * @ticket 61393 + * @ticket 63123 + * + * @dataProvider data_url_fragments + * + * @param string $url_fragment URL fragment - should only be the value following the `#`. + * @param string $not_expected URL fragment that is unexpected - should only be the value unexpected following the `#`. + */ + public function test_pagination_links_does_not_modify_url_fragments_without_trailing_slash_permalinks( $url_fragment, $not_expected ) { + $this->set_permalink_structure( '/%postname%' ); + + $args = array( + 'base' => "http://example.org/%_%#{$url_fragment}", + 'format' => 'page/%#%', + 'total' => 5, + 'current' => 3, + 'prev_next' => true, + ); + + $links = paginate_links( $args ); + + $this->assertStringNotContainsString( + "#{$not_expected}\"", + $links, + 'URL fragments should not be modified to include a trailing slash.' + ); + } + + /** + * Data provider for test_pagination_links_does_not_modify_url_fragments_* tests. + * + * @return array[] Data provider. + */ + public function data_url_fragments() { + return array( + 'url fragment without trailing slash' => array( 'url-fragment', 'url-fragment/' ), + 'url fragment with trailing slash' => array( 'url-fragment/', 'url-fragment' ), + ); + } }