• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / 59517635615d055f783a2be92e52a1e2637df1be

17 Feb 2025 11:08AM UTC coverage: 53.377% (-1.3%) from 54.636%
59517635615d055f783a2be92e52a1e2637df1be

Pull #22048

github

web-flow
Merge e41dbb150 into 711656c23
Pull Request #22048: Update Dashboard page description

7808 of 13867 branches covered (56.31%)

Branch coverage included in aggregate %.

4 of 5 new or added lines in 2 files covered. (80.0%)

1554 existing lines in 42 files now uncovered.

30279 of 57488 relevant lines covered (52.67%)

40022.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.97
/inc/sitemaps/class-sitemaps.php
1
<?php
2
/**
3
 * WPSEO plugin file.
4
 *
5
 * @package WPSEO\XML_Sitemaps
6
 */
7

8
/**
9
 * Class WPSEO_Sitemaps.
10
 *
11
 * @todo This class could use a general description with some explanation on sitemaps. OR.
12
 */
13
class WPSEO_Sitemaps {
14

15
        /**
16
         * Sitemap index identifier.
17
         *
18
         * @var string
19
         */
20
        public const SITEMAP_INDEX_TYPE = '1';
21

22
        /**
23
         * Content of the sitemap to output.
24
         *
25
         * @var string
26
         */
27
        protected $sitemap = '';
28

29
        /**
30
         * Flag to indicate if this is an invalid or empty sitemap.
31
         *
32
         * @var bool
33
         */
34
        public $bad_sitemap = false;
35

36
        /**
37
         * Whether or not the XML sitemap was served from a transient or not.
38
         *
39
         * @var bool
40
         */
41
        private $transient = false;
42

43
        /**
44
         * HTTP protocol to use in headers.
45
         *
46
         * @since 3.2
47
         *
48
         * @var string
49
         */
50
        protected $http_protocol = 'HTTP/1.1';
51

52
        /**
53
         * Holds the n variable.
54
         *
55
         * @var int
56
         */
57
        private $current_page = 1;
58

59
        /**
60
         * The sitemaps router.
61
         *
62
         * @since 3.2
63
         *
64
         * @var WPSEO_Sitemaps_Router
65
         */
66
        public $router;
67

68
        /**
69
         * The sitemap renderer.
70
         *
71
         * @since 3.2
72
         *
73
         * @var WPSEO_Sitemaps_Renderer
74
         */
75
        public $renderer;
76

77
        /**
78
         * The sitemap cache.
79
         *
80
         * @since 3.2
81
         *
82
         * @var WPSEO_Sitemaps_Cache
83
         */
84
        public $cache;
85

86
        /**
87
         * The sitemap providers.
88
         *
89
         * @since 3.2
90
         *
91
         * @var WPSEO_Sitemap_Provider[]
92
         */
93
        public $providers;
94

95
        /**
96
         * Class constructor.
97
         */
98
        public function __construct() {
4✔
99

100
                add_action( 'after_setup_theme', [ $this, 'init_sitemaps_providers' ] );
4✔
101
                add_action( 'after_setup_theme', [ $this, 'reduce_query_load' ], 99 );
4✔
102
                add_action( 'pre_get_posts', [ $this, 'redirect' ], 1 );
4✔
103
                add_action( 'wpseo_hit_sitemap_index', [ $this, 'hit_sitemap_index' ] );
4✔
104

105
                $this->router   = new WPSEO_Sitemaps_Router();
4✔
106
                $this->renderer = new WPSEO_Sitemaps_Renderer();
4✔
107
                $this->cache    = new WPSEO_Sitemaps_Cache();
4✔
108

109
                if ( ! empty( $_SERVER['SERVER_PROTOCOL'] ) ) {
4✔
110
                        $this->http_protocol = sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) );
4✔
111
                }
112
        }
113

114
        /**
115
         * Initialize sitemap providers classes.
116
         *
117
         * @since 5.3
118
         *
119
         * @return void
120
         */
121
        public function init_sitemaps_providers() {
4✔
122

123
                $this->providers = [
4✔
124
                        new WPSEO_Post_Type_Sitemap_Provider(),
4✔
125
                        new WPSEO_Taxonomy_Sitemap_Provider(),
4✔
126
                        new WPSEO_Author_Sitemap_Provider(),
4✔
127
                ];
4✔
128

129
                $external_providers = apply_filters( 'wpseo_sitemaps_providers', [] );
4✔
130

131
                foreach ( $external_providers as $provider ) {
4✔
132
                        if ( is_object( $provider ) && $provider instanceof WPSEO_Sitemap_Provider ) {
×
133
                                $this->providers[] = $provider;
×
134
                        }
135
                }
136
        }
137

138
        /**
139
         * Check the current request URI, if we can determine it's probably an XML sitemap, kill loading the widgets.
140
         *
141
         * @return void
142
         */
143
        public function reduce_query_load() {
×
144
                if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
×
145
                        return;
×
146
                }
147
                $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
×
148
                $extension   = substr( $request_uri, -4 );
×
149
                if ( stripos( $request_uri, 'sitemap' ) !== false && in_array( $extension, [ '.xml', '.xsl' ], true ) ) {
×
150
                        remove_all_actions( 'widgets_init' );
×
151
                }
152
        }
153

154
        /**
155
         * Register your own sitemap. Call this during 'init'.
156
         *
157
         * @param string   $name              The name of the sitemap.
158
         * @param callback $building_function Function to build your sitemap.
159
         * @param string   $rewrite           Optional. Regular expression to match your sitemap with.
160
         *
161
         * @return void
162
         */
163
        public function register_sitemap( $name, $building_function, $rewrite = '' ) {
×
164
                add_action( 'wpseo_do_sitemap_' . $name, $building_function );
×
165
                if ( $rewrite ) {
×
166
                        Yoast_Dynamic_Rewrites::instance()->add_rule( $rewrite, 'index.php?sitemap=' . $name, 'top' );
×
167
                }
168
        }
169

170
        /**
171
         * Register your own XSL file. Call this during 'init'.
172
         *
173
         * @since 1.4.23
174
         *
175
         * @param string   $name              The name of the XSL file.
176
         * @param callback $building_function Function to build your XSL file.
177
         * @param string   $rewrite           Optional. Regular expression to match your sitemap with.
178
         *
179
         * @return void
180
         */
181
        public function register_xsl( $name, $building_function, $rewrite = '' ) {
×
182
                add_action( 'wpseo_xsl_' . $name, $building_function );
×
183
                if ( $rewrite ) {
×
184
                        Yoast_Dynamic_Rewrites::instance()->add_rule( $rewrite, 'index.php?yoast-sitemap-xsl=' . $name, 'top' );
×
185
                }
186
        }
187

188
        /**
189
         * Set the sitemap current page to allow creating partial sitemaps with WP-CLI
190
         * in a one-off process.
191
         *
192
         * @param int $current_page The part that should be generated.
193
         *
194
         * @return void
195
         */
196
        public function set_n( $current_page ) {
4✔
197
                if ( is_scalar( $current_page ) && intval( $current_page ) > 0 ) {
4✔
198
                        $this->current_page = intval( $current_page );
×
199
                }
200
        }
201

202
        /**
203
         * Set the sitemap content to display after you have generated it.
204
         *
205
         * @param string $sitemap The generated sitemap to output.
206
         *
207
         * @return void
208
         */
209
        public function set_sitemap( $sitemap ) {
×
210
                $this->sitemap = $sitemap;
×
211
        }
212

213
        /**
214
         * Set as true to make the request 404. Used stop the display of empty sitemaps or invalid requests.
215
         *
216
         * @param bool $is_bad Is this a bad request. True or false.
217
         *
218
         * @return void
219
         */
220
        public function set_bad_sitemap( $is_bad ) {
×
221
                $this->bad_sitemap = (bool) $is_bad;
×
222
        }
223

224
        /**
225
         * Prevent stupid plugins from running shutdown scripts when we're obviously not outputting HTML.
226
         *
227
         * @since 1.4.16
228
         *
229
         * @return void
230
         */
231
        public function sitemap_close() {
×
232
                remove_all_actions( 'wp_footer' );
×
233
                die();
×
234
        }
235

236
        /**
237
         * Hijack requests for potential sitemaps and XSL files.
238
         *
239
         * @param WP_Query $query Main query instance.
240
         *
241
         * @return void
242
         */
243
        public function redirect( $query ) {
20✔
244

245
                if ( ! $query->is_main_query() ) {
20✔
246
                        return;
16✔
247
                }
248

249
                $yoast_sitemap_xsl = get_query_var( 'yoast-sitemap-xsl' );
16✔
250

251
                if ( ! empty( $yoast_sitemap_xsl ) ) {
16✔
252
                        /*
253
                         * This is a method to provide the XSL via the home_url.
254
                         * Needed when the site_url and home_url are not the same.
255
                         * Loading the XSL needs to come from the same domain, protocol and port as the XML.
256
                         *
257
                         * Whenever home_url and site_url are the same, the file can be loaded directly.
258
                         */
259
                        $this->xsl_output( $yoast_sitemap_xsl );
×
260
                        $this->sitemap_close();
×
261

262
                        return;
×
263
                }
264

265
                $type = get_query_var( 'sitemap' );
16✔
266

267
                if ( empty( $type ) ) {
16✔
268
                        return;
×
269
                }
270

271
                if ( get_query_var( 'sitemap_n' ) === '1' || get_query_var( 'sitemap_n' ) === '0' ) {
16✔
272
                        wp_safe_redirect( home_url( "/$type-sitemap.xml" ), 301, 'Yoast SEO' );
×
273
                        exit;
×
274
                }
275

276
                $this->set_n( get_query_var( 'sitemap_n' ) );
16✔
277

278
                if ( ! $this->get_sitemap_from_cache( $type, $this->current_page ) ) {
16✔
279
                        $this->build_sitemap( $type );
16✔
280
                }
281

282
                if ( $this->bad_sitemap ) {
16✔
283
                        $query->set_404();
×
284
                        status_header( 404 );
×
285

286
                        return;
×
287
                }
288

289
                $this->output();
16✔
290
                $this->sitemap_close();
16✔
291
        }
292

293
        /**
294
         * Try to get the sitemap from cache.
295
         *
296
         * @param string $type        Sitemap type.
297
         * @param int    $page_number The page number to retrieve.
298
         *
299
         * @return bool If the sitemap has been retrieved from cache.
300
         */
301
        private function get_sitemap_from_cache( $type, $page_number ) {
4✔
302

303
                $this->transient = false;
4✔
304

305
                if ( $this->cache->is_enabled() !== true ) {
4✔
306
                        return false;
4✔
307
                }
308

309
                /**
310
                 * Fires before the attempt to retrieve XML sitemap from the transient cache.
311
                 *
312
                 * @param WPSEO_Sitemaps $sitemaps Sitemaps object.
313
                 */
314
                do_action( 'wpseo_sitemap_stylesheet_cache_' . $type, $this );
×
315

316
                $sitemap_cache_data = $this->cache->get_sitemap_data( $type, $page_number );
×
317

318
                // No cache was found, refresh it because cache is enabled.
319
                if ( empty( $sitemap_cache_data ) ) {
×
320
                        return $this->refresh_sitemap_cache( $type, $page_number );
×
321
                }
322

323
                // Cache object was found, parse information.
324
                $this->transient = true;
×
325

326
                $this->sitemap     = $sitemap_cache_data->get_sitemap();
×
327
                $this->bad_sitemap = ! $sitemap_cache_data->is_usable();
×
328

329
                return true;
×
330
        }
331

332
        /**
333
         * Build and save sitemap to cache.
334
         *
335
         * @param string $type        Sitemap type.
336
         * @param int    $page_number The page number to save to.
337
         *
338
         * @return bool
339
         */
340
        private function refresh_sitemap_cache( $type, $page_number ) {
×
341
                $this->set_n( $page_number );
×
342
                $this->build_sitemap( $type );
×
343

344
                return $this->cache->store_sitemap( $type, $page_number, $this->sitemap, ! $this->bad_sitemap );
×
345
        }
346

347
        /**
348
         * Attempts to build the requested sitemap.
349
         *
350
         * Sets $bad_sitemap if this isn't for the root sitemap, a post type or taxonomy.
351
         *
352
         * @param string $type The requested sitemap's identifier.
353
         *
354
         * @return void
355
         */
356
        public function build_sitemap( $type ) {
4✔
357

358
                /**
359
                 * Filter the type of sitemap to build.
360
                 *
361
                 * @param string $type Sitemap type, determined by the request.
362
                 */
363
                $type = apply_filters( 'wpseo_build_sitemap_post_type', $type );
4✔
364

365
                if ( $type === '1' ) {
4✔
366
                        $this->build_root_map();
2✔
367

368
                        return;
2✔
369
                }
370

371
                $entries_per_page = $this->get_entries_per_page();
2✔
372

373
                foreach ( $this->providers as $provider ) {
2✔
374
                        if ( ! $provider->handles_type( $type ) ) {
2✔
375
                                continue;
2✔
376
                        }
377

378
                        try {
379
                                $links = $provider->get_sitemap_links( $type, $entries_per_page, $this->current_page );
2✔
380
                        } catch ( OutOfBoundsException $exception ) {
×
381
                                $this->bad_sitemap = true;
×
382

383
                                return;
×
384
                        }
385

386
                        $this->sitemap = $this->renderer->get_sitemap( $links, $type, $this->current_page );
2✔
387

388
                        return;
2✔
389
                }
390

391
                if ( has_action( 'wpseo_do_sitemap_' . $type ) ) {
×
392
                        /**
393
                         * Fires custom handler, if hooked to generate sitemap for the type.
394
                         */
395
                        do_action( 'wpseo_do_sitemap_' . $type );
×
396

397
                        return;
×
398
                }
399

400
                $this->bad_sitemap = true;
×
401
        }
402

403
        /**
404
         * Build the root sitemap (example.com/sitemap_index.xml) which lists sub-sitemaps for other content types.
405
         *
406
         * @return void
407
         */
408
        public function build_root_map() {
4✔
409

410
                $links            = [];
4✔
411
                $entries_per_page = $this->get_entries_per_page();
4✔
412

413
                foreach ( $this->providers as $provider ) {
4✔
414
                        $links = array_merge( $links, $provider->get_index_links( $entries_per_page ) );
4✔
415
                }
416

417
                /**
418
                 * Filter the sitemap links array before the index sitemap is built.
419
                 *
420
                 * @param array  $links Array of sitemap links
421
                 */
422
                $links = apply_filters( 'wpseo_sitemap_index_links', $links );
4✔
423

424
                if ( empty( $links ) ) {
4✔
425
                        $this->bad_sitemap = true;
×
426
                        $this->sitemap     = '';
×
427

428
                        return;
×
429
                }
430

431
                $this->sitemap = $this->renderer->get_index( $links );
4✔
432
        }
433

434
        /**
435
         * Spits out the XSL for the XML sitemap.
436
         *
437
         * @since 1.4.13
438
         *
439
         * @param string $type Type to output.
440
         *
441
         * @return void
442
         */
443
        public function xsl_output( $type ) {
×
444

445
                if ( $type !== 'main' ) {
×
446

447
                        /**
448
                         * Fires for the output of XSL for XML sitemaps, other than type "main".
449
                         */
450
                        do_action( 'wpseo_xsl_' . $type );
×
451

452
                        return;
×
453
                }
454

455
                header( $this->http_protocol . ' 200 OK', true, 200 );
×
456
                // Prevent the search engines from indexing the XML Sitemap.
457
                header( 'X-Robots-Tag: noindex, follow', true );
×
458
                header( 'Content-Type: text/xml' );
×
459

460
                // Make the browser cache this file properly.
461
                $expires = YEAR_IN_SECONDS;
×
462
                header( 'Pragma: public' );
×
463
                header( 'Cache-Control: max-age=' . $expires );
×
464
                header( 'Expires: ' . YoastSEO()->helpers->date->format_timestamp( ( time() + $expires ), 'D, d M Y H:i:s' ) . ' GMT' );
×
465

466
                // Don't use WP_Filesystem() here because that's not initialized yet. See https://yoast.atlassian.net/browse/QAK-2043.
467
                readfile( WPSEO_PATH . 'css/main-sitemap.xsl' );
×
468
        }
469

470
        /**
471
         * Spit out the generated sitemap.
472
         *
473
         * @return void
474
         */
475
        public function output() {
4✔
476
                $this->send_headers();
4✔
477
                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping sitemap as either xml or html results in empty document.
478
                echo $this->renderer->get_output( $this->sitemap );
4✔
479
        }
480

481
        /**
482
         * Makes a request to the sitemap index to cache it before the arrival of the search engines.
483
         *
484
         * @return void
485
         */
486
        public function hit_sitemap_index() {
×
487
                if ( ! $this->cache->is_enabled() ) {
×
488
                        return;
×
489
                }
490

491
                wp_remote_get( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) );
×
492
        }
493

494
        /**
495
         * Get the GMT modification date for the last modified post in the post type.
496
         *
497
         * @since 3.2
498
         *
499
         * @param string|array $post_types Post type or array of types.
500
         * @param bool         $return_all Flag to return array of values.
501
         *
502
         * @return string|array|false
503
         */
504
        public static function get_last_modified_gmt( $post_types, $return_all = false ) {
6✔
505

506
                global $wpdb;
6✔
507

508
                static $post_type_dates = null;
6✔
509

510
                if ( ! is_array( $post_types ) ) {
6✔
511
                        $post_types = [ $post_types ];
×
512
                }
513

514
                foreach ( $post_types as $post_type ) {
6✔
515
                        if ( ! isset( $post_type_dates[ $post_type ] ) ) { // If we hadn't seen post type before. R.
6✔
516
                                $post_type_dates = null;
6✔
517
                                break;
6✔
518
                        }
519
                }
520

521
                if ( is_null( $post_type_dates ) ) {
6✔
522

523
                        $post_type_dates = [];
6✔
524
                        $post_type_names = WPSEO_Post_Type::get_accessible_post_types();
6✔
525

526
                        if ( ! empty( $post_type_names ) ) {
6✔
527
                                $post_statuses = array_map( 'esc_sql', self::get_post_statuses() );
6✔
528
                                $replacements  = array_merge(
6✔
529
                                        [
6✔
530
                                                'post_type',
6✔
531
                                                'post_modified_gmt',
6✔
532
                                                'date',
6✔
533
                                                $wpdb->posts,
6✔
534
                                                'post_status',
6✔
535
                                        ],
6✔
536
                                        $post_statuses,
6✔
537
                                        [ 'post_type' ],
6✔
538
                                        array_keys( $post_type_names ),
6✔
539
                                        [
6✔
540
                                                'post_type',
6✔
541
                                                'date',
6✔
542
                                        ]
6✔
543
                                );
6✔
544

545
                                //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here.
546
                                //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
547
                                $dates = $wpdb->get_results(
6✔
548
                                        //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized.
549
                                        $wpdb->prepare(
6✔
550
                                                '
6✔
551
                                        SELECT %i, MAX(%i) AS %i
552
                                        FROM %i
553
                                        WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ')
6✔
554
                                                AND %i IN (' . implode( ', ', array_fill( 0, count( $post_type_names ), '%s' ) ) . ')
6✔
555
                                        GROUP BY %i
556
                                        ORDER BY %i DESC
557
                                ',
6✔
558
                                                $replacements
6✔
559
                                        )
6✔
560
                                );
6✔
561

562
                                foreach ( $dates as $obj ) {
6✔
563
                                        $post_type_dates[ $obj->post_type ] = $obj->date;
4✔
564
                                }
565
                        }
566
                }
567

568
                $dates = array_intersect_key( $post_type_dates, array_flip( $post_types ) );
6✔
569

570
                if ( count( $dates ) > 0 ) {
6✔
571
                        if ( $return_all ) {
4✔
572
                                return $dates;
2✔
573
                        }
574

575
                        return max( $dates );
4✔
576
                }
577

578
                return false;
2✔
579
        }
580

581
        /**
582
         * Get the modification date for the last modified post in the post type.
583
         *
584
         * @param array $post_types Post types to get the last modification date for.
585
         *
586
         * @return string
587
         */
588
        public function get_last_modified( $post_types ) {
×
589
                return YoastSEO()->helpers->date->format( self::get_last_modified_gmt( $post_types ) );
×
590
        }
591

592
        /**
593
         * Get the maximum number of entries per XML sitemap.
594
         *
595
         * @return int The maximum number of entries.
596
         */
597
        protected function get_entries_per_page() {
4✔
598
                /**
599
                 * Filter the maximum number of entries per XML sitemap.
600
                 *
601
                 * After changing the output of the filter, make sure that you disable and enable the
602
                 * sitemaps to make sure the value is picked up for the sitemap cache.
603
                 *
604
                 * @param int $entries The maximum number of entries per XML sitemap.
605
                 */
606
                $entries = (int) apply_filters( 'wpseo_sitemap_entries_per_page', 1000 );
4✔
607

608
                return $entries;
4✔
609
        }
610

611
        /**
612
         * Get post statuses for post_type or the root sitemap.
613
         *
614
         * @since 10.2
615
         *
616
         * @param string $type Provide a type for a post_type sitemap, SITEMAP_INDEX_TYPE for the root sitemap.
617
         *
618
         * @return array List of post statuses.
619
         */
620
        public static function get_post_statuses( $type = self::SITEMAP_INDEX_TYPE ) {
2✔
621
                /**
622
                 * Filter post status list for sitemap query for the post type.
623
                 *
624
                 * @param array  $post_statuses Post status list, defaults to array( 'publish' ).
625
                 * @param string $type          Post type or SITEMAP_INDEX_TYPE.
626
                 */
627
                $post_statuses = apply_filters( 'wpseo_sitemap_post_statuses', [ 'publish' ], $type );
2✔
628

629
                if ( ! is_array( $post_statuses ) || empty( $post_statuses ) ) {
2✔
630
                        $post_statuses = [ 'publish' ];
×
631
                }
632

633
                if ( ( $type === self::SITEMAP_INDEX_TYPE || $type === 'attachment' )
2✔
634
                        && ! in_array( 'inherit', $post_statuses, true )
2✔
635
                ) {
636
                        $post_statuses[] = 'inherit';
2✔
637
                }
638

639
                return $post_statuses;
2✔
640
        }
641

642
        /**
643
         * Sends all the required HTTP Headers.
644
         *
645
         * @return void
646
         */
647
        private function send_headers() {
4✔
648
                if ( headers_sent() ) {
4✔
649
                        return;
4✔
650
                }
651

UNCOV
652
                $headers = [
×
653
                        $this->http_protocol . ' 200 OK' => 200,
×
654
                        // Prevent the search engines from indexing the XML Sitemap.
655
                        'X-Robots-Tag: noindex, follow'  => '',
×
656
                        'Content-Type: text/xml; charset=' . esc_attr( $this->renderer->get_output_charset() ) => '',
×
UNCOV
657
                ];
×
658

659
                /**
660
                 * Filter the HTTP headers we send before an XML sitemap.
661
                 *
662
                 * @param array  $headers The HTTP headers we're going to send out.
663
                 */
664
                $headers = apply_filters( 'wpseo_sitemap_http_headers', $headers );
×
665

666
                foreach ( $headers as $header => $status ) {
×
667
                        if ( is_numeric( $status ) ) {
×
668
                                header( $header, true, $status );
×
669
                                continue;
×
670
                        }
671
                        header( $header, true );
×
672
                }
673
        }
674
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc