diff --git a/src/wp-admin/includes/class-wp-list-table.php b/src/wp-admin/includes/class-wp-list-table.php index 5e6bcdb0d237c..64d13a8f6651c 100644 --- a/src/wp-admin/includes/class-wp-list-table.php +++ b/src/wp-admin/includes/class-wp-list-table.php @@ -1054,7 +1054,7 @@ protected function pagination( $which ) { $current = $this->get_pagenum(); $removable_query_args = wp_removable_query_args(); - $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); + $current_url = $this->get_current_url(); $current_url = remove_query_arg( $removable_query_args, $current_url ); @@ -1184,6 +1184,36 @@ protected function pagination( $which ) { echo $this->_pagination; } + /** + * Builds the current request URL for list table links. + * + * When `HTTP_HOST` omits the port, non-default ports from `SERVER_PORT` + * are preserved so search, sorting, and pagination keep working. + * + * @since 7.2.0 + * + * @return string Current URL including scheme, host, port, and request URI. + */ + protected function get_current_url() { + $host = isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : ''; + $uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : ''; + + if ( isset( $_SERVER['SERVER_PORT'] ) ) { + $port = (int) $_SERVER['SERVER_PORT']; + $parsed = wp_parse_url( 'http://' . $host ); + + if ( $port && empty( $parsed['port'] ) ) { + $default_port = is_ssl() ? 443 : 80; + + if ( $default_port !== $port ) { + $host .= ':' . $port; + } + } + } + + return set_url_scheme( 'http://' . $host . $uri ); + } + /** * Gets a list of columns. * @@ -1408,7 +1438,7 @@ public function get_column_count() { public function print_column_headers( $with_id = true ) { list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); - $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); + $current_url = $this->get_current_url(); $current_url = remove_query_arg( 'paged', $current_url ); // When users click on a column header to sort by other columns. diff --git a/tests/phpunit/tests/admin/wpListTable.php b/tests/phpunit/tests/admin/wpListTable.php index b3de66659b308..f1d17f760d8b2 100644 --- a/tests/phpunit/tests/admin/wpListTable.php +++ b/tests/phpunit/tests/admin/wpListTable.php @@ -592,4 +592,103 @@ public function test_search_box_works_with_orderby_string() { $this->assertStringContainsString( $expected_html, $actual ); } + + /** + * Tests that list table pagination links preserve a non-default port + * when HTTP_HOST does not include it. + * + * @ticket 59272 + * + * @covers WP_List_Table::get_current_url + * @covers WP_List_Table::pagination + */ + public function test_pagination_links_preserve_non_default_port() { + $list_table = new WP_List_Table_Current_Url_Test_Double(); + + $_SERVER['HTTP_HOST'] = 'localhost'; + $_SERVER['REQUEST_URI'] = '/wp-admin/edit.php'; + $_SERVER['SERVER_PORT'] = '8182'; + unset( $_SERVER['HTTPS'] ); + + $output = $list_table->get_pagination_html(); + + $this->assertStringContainsString( 'localhost:8182/wp-admin/edit.php', $output ); + $this->assertStringNotContainsString( 'http://localhost/wp-admin/edit.php', $output ); + } + + /** + * Tests that list table pagination links do not duplicate a port already + * present in HTTP_HOST. + * + * @ticket 59272 + * + * @covers WP_List_Table::get_current_url + * @covers WP_List_Table::pagination + */ + public function test_pagination_links_do_not_duplicate_port_in_host() { + $list_table = new WP_List_Table_Current_Url_Test_Double(); + + $_SERVER['HTTP_HOST'] = 'localhost:8182'; + $_SERVER['REQUEST_URI'] = '/wp-admin/edit.php'; + $_SERVER['SERVER_PORT'] = '8182'; + unset( $_SERVER['HTTPS'] ); + + $output = $list_table->get_pagination_html(); + + $this->assertStringContainsString( 'localhost:8182/wp-admin/edit.php', $output ); + $this->assertStringNotContainsString( 'localhost:8182:8182', $output ); + } + + /** + * Tests that default ports are not appended when HTTP_HOST has no port. + * + * @ticket 59272 + * + * @covers WP_List_Table::get_current_url + * @covers WP_List_Table::pagination + */ + public function test_pagination_links_do_not_append_default_http_port() { + $list_table = new WP_List_Table_Current_Url_Test_Double(); + + $_SERVER['HTTP_HOST'] = 'example.org'; + $_SERVER['REQUEST_URI'] = '/wp-admin/edit.php'; + $_SERVER['SERVER_PORT'] = '80'; + unset( $_SERVER['HTTPS'] ); + + $output = $list_table->get_pagination_html(); + + $this->assertStringContainsString( 'example.org/wp-admin/edit.php', $output ); + $this->assertStringNotContainsString( 'example.org:80/', $output ); + } +} + +/** + * Test double that exposes pagination HTML for URL assertions. + */ +class WP_List_Table_Current_Url_Test_Double extends WP_List_Table { + public function get_columns() { + return array( + 'title' => 'Title', + ); + } + + /** + * Returns pagination markup for the current request URL. + * + * @return string Pagination HTML. + */ + public function get_pagination_html() { + $this->_pagination = null; + $this->set_pagination_args( + array( + 'total_items' => 40, + 'total_pages' => 4, + 'per_page' => 10, + ) + ); + + ob_start(); + $this->pagination( 'bottom' ); + return ob_get_clean(); + } }