Make WordPress Core

Changeset 63575


Ignore:
Timestamp:
09/10/2026 01:09:53 AM (4 weeks 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.1 branch.
Merged to the 7.0 branch in error in r63573 and reverted in r63574.

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

Location:
branches/7.1
Files:
4 edited
1 copied

Legend:

Unmodified
Added
Removed
  • branches/7.1

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

    r61445 r63575  
    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.1/src/wp-includes/sitemaps/class-wp-sitemaps.php

    r59229 r63575  
    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.1/tests/phpunit/tests/sitemaps/sitemaps.php

    r57987 r63575  
    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.