PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.9
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.9
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / inc / sitemaps / class-sitemaps.php

class-sitemaps.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.9, at inc/sitemaps/class-sitemaps.php

675 lines 16.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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() {
99
100 add_action( 'after_setup_theme', [ $this, 'init_sitemaps_providers' ] );
101 add_action( 'after_setup_theme', [ $this, 'reduce_query_load' ], 99 );
102 add_action( 'pre_get_posts', [ $this, 'redirect' ], 1 );
103 add_action( 'wpseo_hit_sitemap_index', [ $this, 'hit_sitemap_index' ] );
104
105 $this->router = new WPSEO_Sitemaps_Router();
106 $this->renderer = new WPSEO_Sitemaps_Renderer();
107 $this->cache = new WPSEO_Sitemaps_Cache();
108
109 if ( ! empty( $_SERVER['SERVER_PROTOCOL'] ) ) {
110 $this->http_protocol = sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) );
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() {
122
123 $this->providers = [
124 new WPSEO_Post_Type_Sitemap_Provider(),
125 new WPSEO_Taxonomy_Sitemap_Provider(),
126 new WPSEO_Author_Sitemap_Provider(),
127 ];
128
129 $external_providers = apply_filters( 'wpseo_sitemaps_providers', [] );
130
131 foreach ( $external_providers as $provider ) {
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 ) {
197 if ( is_scalar( $current_page ) && (int) $current_page > 0 ) {
198 $this->current_page = (int) $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 exit();
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 ) {
244
245 if ( ! $query->is_main_query() ) {
246 return;
247 }
248
249 $yoast_sitemap_xsl = get_query_var( 'yoast-sitemap-xsl' );
250
251 if ( ! empty( $yoast_sitemap_xsl ) ) {
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' );
266
267 if ( empty( $type ) ) {
268 return;
269 }
270
271 if ( get_query_var( 'sitemap_n' ) === '1' || get_query_var( 'sitemap_n' ) === '0' ) {
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' ) );
277
278 if ( ! $this->get_sitemap_from_cache( $type, $this->current_page ) ) {
279 $this->build_sitemap( $type );
280 }
281
282 if ( $this->bad_sitemap ) {
283 $query->set_404();
284 status_header( 404 );
285
286 return;
287 }
288
289 $this->output();
290 $this->sitemap_close();
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 ) {
302
303 $this->transient = false;
304
305 if ( $this->cache->is_enabled() !== true ) {
306 return false;
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 ) {
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 );
364
365 if ( $type === '1' ) {
366 $this->build_root_map();
367
368 return;
369 }
370
371 $entries_per_page = $this->get_entries_per_page();
372
373 foreach ( $this->providers as $provider ) {
374 if ( ! $provider->handles_type( $type ) ) {
375 continue;
376 }
377
378 try {
379 $links = $provider->get_sitemap_links( $type, $entries_per_page, $this->current_page );
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 );
387
388 return;
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() {
409
410 $links = [];
411 $entries_per_page = $this->get_entries_per_page();
412
413 foreach ( $this->providers as $provider ) {
414 $links = array_merge( $links, $provider->get_index_links( $entries_per_page ) );
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 );
423
424 if ( empty( $links ) ) {
425 $this->bad_sitemap = true;
426 $this->sitemap = '';
427
428 return;
429 }
430
431 $this->sitemap = $this->renderer->get_index( $links );
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() {
476 $this->send_headers();
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 );
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 ) {
505
506 global $wpdb;
507
508 static $post_type_dates = null;
509
510 if ( ! is_array( $post_types ) ) {
511 $post_types = [ $post_types ];
512 }
513
514 foreach ( $post_types as $post_type ) {
515 if ( ! isset( $post_type_dates[ $post_type ] ) ) { // If we hadn't seen post type before. R.
516 $post_type_dates = null;
517 break;
518 }
519 }
520
521 if ( $post_type_dates === null ) {
522
523 $post_type_dates = [];
524 $post_type_names = WPSEO_Post_Type::get_accessible_post_types();
525
526 if ( ! empty( $post_type_names ) ) {
527 $post_statuses = array_map( 'esc_sql', self::get_post_statuses() );
528 $replacements = array_merge(
529 [
530 'post_type',
531 'post_modified_gmt',
532 'date',
533 $wpdb->posts,
534 'post_status',
535 ],
536 $post_statuses,
537 [ 'post_type' ],
538 array_keys( $post_type_names ),
539 [
540 'post_type',
541 'date',
542 ],
543 );
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(
548 //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized.
549 $wpdb->prepare(
550 '
551 SELECT %i, MAX(%i) AS %i
552 FROM %i
553 WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ')
554 AND %i IN (' . implode( ', ', array_fill( 0, count( $post_type_names ), '%s' ) ) . ')
555 GROUP BY %i
556 ORDER BY %i DESC
557 ',
558 $replacements,
559 ),
560 );
561
562 foreach ( $dates as $obj ) {
563 $post_type_dates[ $obj->post_type ] = $obj->date;
564 }
565 }
566 }
567
568 $dates = array_intersect_key( $post_type_dates, array_flip( $post_types ) );
569
570 if ( count( $dates ) > 0 ) {
571 if ( $return_all ) {
572 return $dates;
573 }
574
575 return max( $dates );
576 }
577
578 return false;
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() {
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 );
607
608 return $entries;
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 ) {
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 );
628
629 if ( ! is_array( $post_statuses ) || empty( $post_statuses ) ) {
630 $post_statuses = [ 'publish' ];
631 }
632
633 if ( ( $type === self::SITEMAP_INDEX_TYPE || $type === 'attachment' )
634 && ! in_array( 'inherit', $post_statuses, true )
635 ) {
636 $post_statuses[] = 'inherit';
637 }
638
639 return $post_statuses;
640 }
641
642 /**
643 * Sends all the required HTTP Headers.
644 *
645 * @return void
646 */
647 private function send_headers() {
648 if ( headers_sent() ) {
649 return;
650 }
651
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() ) => '',
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 }
675