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(
'%s',
@@ -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(
'%s',
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' ),
+ );
+ }
}