Make WordPress Core

Changeset 63573


Ignore:
Timestamp:
09/10/2026 12:38:33 AM (8 hours ago)
Author:
westonruter
Message:

Sitemaps: Don't 404 valid sitemaps on sites with no posts.

WP::handle_404() sets a 404 when the main query matches no posts and no exception applies. Sitemap requests were never among those exceptions; they were shielded only incidentally, by falling through to is_home, and in r62664 that fallthrough was removed. A site with no published posts therefore served a complete, valid sitemap under a 404 status, which search engines discard.

Exempt sitemap and stylesheet routes there, alongside the existing admin, robots and favicon exceptions. Since handle_404() no longer decides the status for these requests, every sitemap 404 now has to be issued by WP_Sitemaps::render_sitemaps() instead: an unregistered provider, an unrecognized stylesheet type, and a route whose query vars do not survive sanitize_text_field() would each otherwise be served as a 200 on an arbitrary URL. These share a send_404() helper, which also sends the no-cache headers handle_404() was previously contributing, so an intermediary does not retain a 404 for a route that becomes valid once the site has more content.

Sitemaps disabled via the wp_sitemaps_enabled filter, and providers with an empty URL list, keep the status they already had; whether the latter should render an empty sitemap instead is #61293.

Developed in https://github.com/WordPress/wordpress-develop/pull/13247.
Follow-up to r48072, r48523, r62664.

Reviewed by adamsilverstein.
Merges r63570 to the 7.0 branch.

Props iamchitti, westonruter, fernandot, wildworks, harishtewari, l1onofjudah, luksusspokoju, abrahamfariaz, andreasca, siliconforks, adamsilverstein, audrasjb, ocean90, mrkenobi.
See #39157, #61293.
Fixes #65945.

Location:
branches/7.0
Files:
4 edited
1 copied

Legend:

Unmodified
Added
Removed
  • branches/7.0

  • branches/7.0/src/wp-includes/class-wp.php

    r61445 r63573  
    747747                $set_404 = true;
    748748
    749                 // Never 404 for the admin, robots, or favicon.
    750                 if ( is_admin() || is_robots() || is_favicon() ) {
     749                // Never 404 here for the admin, robots, favicon, or sitemaps.
     750                // Sitemap routes send their own status in WP_Sitemaps::render_sitemaps().
     751                if ( is_admin() || is_robots() || is_favicon() || is_sitemap() || get_query_var( 'sitemap-stylesheet' ) ) {
    751752                        $set_404 = false;
    752753
  • branches/7.0/src/wp-includes/sitemaps/class-wp-sitemaps.php

    r59229 r63573  
    158158         *
    159159         * @since 5.5.0
    160          *
    161          * @global WP_Query $wp_query WordPress Query object.
    162160         */
    163161        public function render_sitemaps() {
    164                 global $wp_query;
     162                /*
     163                 * Bail early if this isn't a sitemap or stylesheet route.
     164                 *
     165                 * This runs on every front-end request, so it comes before any
     166                 * sanitizing. The raw query vars are tested here, matching
     167                 * WP::handle_404(), which exempts sitemap requests from its own 404 on
     168                 * the same basis. Testing the sanitized values instead would let a
     169                 * request that handle_404() exempted fall through both, leaving it a 200.
     170                 */
     171                if ( ! get_query_var( 'sitemap' ) && ! get_query_var( 'sitemap-stylesheet' ) ) {
     172                        return;
     173                }
    165174
    166175                $sitemap         = sanitize_text_field( get_query_var( 'sitemap' ) );
     
    169178                $paged           = absint( get_query_var( 'paged' ) );
    170179
    171                 // Bail early if this isn't a sitemap or stylesheet route.
     180                // Force a 404 and bail early if the route did not survive sanitizing.
    172181                if ( ! ( $sitemap || $stylesheet_type ) ) {
     182                        $this->send_404();
    173183                        return;
    174184                }
    175185
    176186                if ( ! $this->sitemaps_enabled() ) {
    177                         $wp_query->set_404();
    178                         status_header( 404 );
     187                        $this->send_404();
    179188                        return;
    180189                }
     
    182191                // Render stylesheet if this is stylesheet route.
    183192                if ( $stylesheet_type ) {
     193                        // Force a 404 and bail early if the stylesheet type is not recognized.
     194                        if ( ! in_array( $stylesheet_type, array( 'sitemap', 'index' ), true ) ) {
     195                                $this->send_404();
     196                                return;
     197                        }
     198
    184199                        $stylesheet = new WP_Sitemaps_Stylesheet();
    185200
     
    198213                $provider = $this->registry->get_provider( $sitemap );
    199214
     215                // Force a 404 and bail early if the requested provider is not registered.
    200216                if ( ! $provider ) {
     217                        $this->send_404();
    201218                        return;
    202219                }
     
    210227                // Force a 404 and bail early if no URLs are present.
    211228                if ( empty( $url_list ) ) {
    212                         $wp_query->set_404();
    213                         status_header( 404 );
     229                        $this->send_404();
    214230                        return;
    215231                }
     
    217233                $this->renderer->render_sitemap( $url_list );
    218234                exit;
     235        }
     236
     237        /**
     238         * Sends a 404 for a sitemap route that cannot be served.
     239         *
     240         * WP::handle_404() exempts sitemap requests, so every sitemap 404 is issued
     241         * here instead. That includes the no-cache headers handle_404() sends with
     242         * its own 404, so an intermediary does not retain a 404 for a route that
     243         * becomes valid once the site has more content.
     244         *
     245         * @since 7.1.1
     246         *
     247         * @global WP_Query $wp_query WordPress Query object.
     248         */
     249        private function send_404(): void {
     250                global $wp_query;
     251
     252                $wp_query->set_404();
     253                status_header( 404 );
     254                nocache_headers();
    219255        }
    220256
  • branches/7.0/tests/phpunit/tests/sitemaps/sitemaps.php

    r57987 r63573  
    494494                $this->assertTrue( is_404() );
    495495        }
     496
     497        /**
     498         * Ensures a paged subtype route with no URLs still 404s, now that
     499         * WP::handle_404() no longer sets a 404 for sitemap requests.
     500         *
     501         * @ticket 65945
     502         */
     503        public function test_empty_url_list_for_subtype_should_return_404() {
     504                wp_register_sitemap_provider( 'foo', new WP_Sitemaps_Empty_Test_Provider( 'foo' ) );
     505
     506                $this->go_to( home_url( '/?sitemap=foo&sitemap-subtype=bar&paged=2' ) );
     507
     508                wp_sitemaps_get_server()->render_sitemaps();
     509
     510                $this->assertTrue( is_404() );
     511        }
     512
     513        /**
     514         * Ensures a sitemap query var that does not survive sanitizing 404s.
     515         *
     516         * WP::handle_404() exempts these requests on the raw query var, while
     517         * render_sitemaps() acts on the sanitized value. Without a matching bail
     518         * they fall through both and an arbitrary URL is served as a 200.
     519         *
     520         * @ticket 65945
     521         *
     522         * @dataProvider data_unusable_sitemap_query_vars
     523         *
     524         * @param non-falsy-string $query_string Query string to append to a nonexistent URL.
     525         */
     526        public function test_unusable_sitemap_query_var_should_return_404( string $query_string ) {
     527                $this->set_permalink_structure( '/%postname%/' );
     528
     529                // Instantiate the server before navigating: registering the sitemap
     530                // rewrite tags is what adds the query vars to `$wp->public_query_vars`.
     531                $sitemaps = wp_sitemaps_get_server();
     532
     533                $this->go_to( home_url( '/this-page-does-not-exist/' . $query_string ) );
     534
     535                $this->assertFalse( is_404(), 'WP::handle_404() should not have set a 404.' );
     536
     537                $sitemaps->render_sitemaps();
     538
     539                $this->assertTrue( is_404(), 'render_sitemaps() should have set a 404.' );
     540        }
     541
     542        /**
     543         * Data provider.
     544         *
     545         * @return array<non-falsy-string, array{ non-falsy-string }>
     546         */
     547        public function data_unusable_sitemap_query_vars(): array {
     548                return array(
     549                        'value stripped by sanitizing' => array( '?sitemap=<>' ),
     550                        'array sitemap value'          => array( '?sitemap[]=index' ),
     551                        'array stylesheet value'       => array( '?sitemap-stylesheet[]=sitemap' ),
     552                );
     553        }
     554
     555        /**
     556         * Ensures an unrecognized stylesheet type 404s from render_sitemaps().
     557         *
     558         * WP::handle_404() exempts any request carrying a `sitemap-stylesheet`
     559         * query var, and WP_Sitemaps_Stylesheet::render_stylesheet() echoes nothing
     560         * for a type other than 'sitemap' or 'index', so this route would otherwise
     561         * be served as a 200 with an empty body.
     562         *
     563         * @ticket 65945
     564         */
     565        public function test_unrecognized_stylesheet_type_should_return_404() {
     566                // Instantiate the server before navigating: registering the sitemap rewrite
     567                // tags is what adds `sitemap-stylesheet` to `$wp->public_query_vars`.
     568                $sitemaps = wp_sitemaps_get_server();
     569
     570                $this->go_to( home_url( '/?sitemap-stylesheet=this-is-not-a-stylesheet' ) );
     571
     572                $this->assertFalse( is_404(), 'WP::handle_404() should not have set a 404.' );
     573
     574                $sitemaps->render_sitemaps();
     575
     576                $this->assertTrue( is_404(), 'render_sitemaps() should have set a 404.' );
     577        }
     578
     579        /**
     580         * Ensures an unregistered provider 404s from render_sitemaps().
     581         *
     582         * WP::handle_404() exempts every sitemap request, so this route would
     583         * otherwise be served with a 200.
     584         *
     585         * @ticket 65945
     586         */
     587        public function test_unregistered_provider_should_return_404() {
     588                // Instantiate the server before navigating: registering the sitemap
     589                // rewrite tags is what adds `sitemap` to `$wp->public_query_vars`.
     590                $sitemaps = wp_sitemaps_get_server();
     591
     592                $this->go_to( home_url( '/?sitemap=this-provider-does-not-exist' ) );
     593
     594                $this->assertFalse( is_404(), 'WP::handle_404() should not have set a 404.' );
     595
     596                $sitemaps->render_sitemaps();
     597
     598                $this->assertTrue( is_404(), 'render_sitemaps() should have set a 404.' );
     599        }
    496600}
Note: See TracChangeset for help on using the changeset viewer.