PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.5.1
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.5.1
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 18.5.1, at inc/sitemaps/class-sitemaps.php

653 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 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 add_action( 'wpseo_ping_search_engines', [ __CLASS__, 'ping_search_engines' ] );
105
106 $this->router = new WPSEO_Sitemaps_Router();
107 $this->renderer = new WPSEO_Sitemaps_Renderer();
108 $this->cache = new WPSEO_Sitemaps_Cache();
109
110 if ( ! empty( $_SERVER['SERVER_PROTOCOL'] ) ) {
111 $this->http_protocol = sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) );
112 }
113 }
114
115 /**
116 * Initialize sitemap providers classes.
117 *
118 * @since 5.3
119 */
120 public function init_sitemaps_providers() {
121
122 $this->providers = [
123 new WPSEO_Post_Type_Sitemap_Provider(),
124 new WPSEO_Taxonomy_Sitemap_Provider(),
125 new WPSEO_Author_Sitemap_Provider(),
126 ];
127
128 $external_providers = apply_filters( 'wpseo_sitemaps_providers', [] );
129
130 foreach ( $external_providers as $provider ) {
131 if ( is_object( $provider ) && $provider instanceof WPSEO_Sitemap_Provider ) {
132 $this->providers[] = $provider;
133 }
134 }
135 }
136
137 /**
138 * Check the current request URI, if we can determine it's probably an XML sitemap, kill loading the widgets.
139 */
140 public function reduce_query_load() {
141 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
142 return;
143 }
144 $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
145 $extension = substr( $request_uri, -4 );
146 if ( stripos( $request_uri, 'sitemap' ) !== false && in_array( $extension, [ '.xml', '.xsl' ], true ) ) {
147 remove_all_actions( 'widgets_init' );
148 }
149 }
150
151 /**
152 * Register your own sitemap. Call this during 'init'.
153 *
154 * @param string $name The name of the sitemap.
155 * @param callback $building_function Function to build your sitemap.
156 * @param string $rewrite Optional. Regular expression to match your sitemap with.
157 */
158 public function register_sitemap( $name, $building_function, $rewrite = '' ) {
159 add_action( 'wpseo_do_sitemap_' . $name, $building_function );
160 if ( ! empty( $rewrite ) ) {
161 add_rewrite_rule( $rewrite, 'index.php?sitemap=' . $name, 'top' );
162 }
163 }
164
165 /**
166 * Register your own XSL file. Call this during 'init'.
167 *
168 * @since 1.4.23
169 *
170 * @param string $name The name of the XSL file.
171 * @param callback $building_function Function to build your XSL file.
172 * @param string $rewrite Optional. Regular expression to match your sitemap with.
173 */
174 public function register_xsl( $name, $building_function, $rewrite = '' ) {
175 add_action( 'wpseo_xsl_' . $name, $building_function );
176 if ( ! empty( $rewrite ) ) {
177 add_rewrite_rule( $rewrite, 'index.php?yoast-sitemap-xsl=' . $name, 'top' );
178 }
179 }
180
181 /**
182 * Set the sitemap current page to allow creating partial sitemaps with WP-CLI
183 * in a one-off process.
184 *
185 * @param int $current_page The part that should be generated.
186 */
187 public function set_n( $current_page ) {
188 if ( is_scalar( $current_page ) && intval( $current_page ) > 0 ) {
189 $this->current_page = intval( $current_page );
190 }
191 }
192
193 /**
194 * Set the sitemap content to display after you have generated it.
195 *
196 * @param string $sitemap The generated sitemap to output.
197 */
198 public function set_sitemap( $sitemap ) {
199 $this->sitemap = $sitemap;
200 }
201
202 /**
203 * Set as true to make the request 404. Used stop the display of empty sitemaps or invalid requests.
204 *
205 * @param bool $is_bad Is this a bad request. True or false.
206 */
207 public function set_bad_sitemap( $is_bad ) {
208 $this->bad_sitemap = (bool) $is_bad;
209 }
210
211 /**
212 * Prevent stupid plugins from running shutdown scripts when we're obviously not outputting HTML.
213 *
214 * @since 1.4.16
215 */
216 public function sitemap_close() {
217 remove_all_actions( 'wp_footer' );
218 die();
219 }
220
221 /**
222 * Hijack requests for potential sitemaps and XSL files.
223 *
224 * @param \WP_Query $query Main query instance.
225 */
226 public function redirect( $query ) {
227
228 if ( ! $query->is_main_query() ) {
229 return;
230 }
231
232 $yoast_sitemap_xsl = get_query_var( 'yoast-sitemap-xsl' );
233
234 if ( ! empty( $yoast_sitemap_xsl ) ) {
235 /*
236 * This is a method to provide the XSL via the home_url.
237 * Needed when the site_url and home_url are not the same.
238 * Loading the XSL needs to come from the same domain, protocol and port as the XML.
239 *
240 * Whenever home_url and site_url are the same, the file can be loaded directly.
241 */
242 $this->xsl_output( $yoast_sitemap_xsl );
243 $this->sitemap_close();
244
245 return;
246 }
247
248 $type = get_query_var( 'sitemap' );
249
250 if ( empty( $type ) ) {
251 return;
252 }
253
254 if ( get_query_var( 'sitemap_n' ) === '1' || get_query_var( 'sitemap_n' ) === '0' ) {
255 wp_safe_redirect( home_url( "/$type-sitemap.xml" ), 301, 'Yoast SEO' );
256 exit;
257 }
258
259 $this->set_n( get_query_var( 'sitemap_n' ) );
260
261 if ( ! $this->get_sitemap_from_cache( $type, $this->current_page ) ) {
262 $this->build_sitemap( $type );
263 }
264
265 if ( $this->bad_sitemap ) {
266 $query->set_404();
267 status_header( 404 );
268
269 return;
270 }
271
272 $this->output();
273 $this->sitemap_close();
274 }
275
276 /**
277 * Try to get the sitemap from cache.
278 *
279 * @param string $type Sitemap type.
280 * @param int $page_number The page number to retrieve.
281 *
282 * @return bool If the sitemap has been retrieved from cache.
283 */
284 private function get_sitemap_from_cache( $type, $page_number ) {
285
286 $this->transient = false;
287
288 if ( $this->cache->is_enabled() !== true ) {
289 return false;
290 }
291
292 /**
293 * Fires before the attempt to retrieve XML sitemap from the transient cache.
294 *
295 * @param WPSEO_Sitemaps $sitemaps Sitemaps object.
296 */
297 do_action( 'wpseo_sitemap_stylesheet_cache_' . $type, $this );
298
299 $sitemap_cache_data = $this->cache->get_sitemap_data( $type, $page_number );
300
301 // No cache was found, refresh it because cache is enabled.
302 if ( empty( $sitemap_cache_data ) ) {
303 return $this->refresh_sitemap_cache( $type, $page_number );
304 }
305
306 // Cache object was found, parse information.
307 $this->transient = true;
308
309 $this->sitemap = $sitemap_cache_data->get_sitemap();
310 $this->bad_sitemap = ! $sitemap_cache_data->is_usable();
311
312 return true;
313 }
314
315 /**
316 * Build and save sitemap to cache.
317 *
318 * @param string $type Sitemap type.
319 * @param int $page_number The page number to save to.
320 *
321 * @return bool
322 */
323 private function refresh_sitemap_cache( $type, $page_number ) {
324 $this->set_n( $page_number );
325 $this->build_sitemap( $type );
326
327 return $this->cache->store_sitemap( $type, $page_number, $this->sitemap, ! $this->bad_sitemap );
328 }
329
330 /**
331 * Attempts to build the requested sitemap.
332 *
333 * Sets $bad_sitemap if this isn't for the root sitemap, a post type or taxonomy.
334 *
335 * @param string $type The requested sitemap's identifier.
336 */
337 public function build_sitemap( $type ) {
338
339 /**
340 * Filter the type of sitemap to build.
341 *
342 * @param string $type Sitemap type, determined by the request.
343 */
344 $type = apply_filters( 'wpseo_build_sitemap_post_type', $type );
345
346 if ( $type === '1' ) {
347 $this->build_root_map();
348
349 return;
350 }
351
352 $entries_per_page = $this->get_entries_per_page();
353
354 foreach ( $this->providers as $provider ) {
355 if ( ! $provider->handles_type( $type ) ) {
356 continue;
357 }
358
359 try {
360 $links = $provider->get_sitemap_links( $type, $entries_per_page, $this->current_page );
361 } catch ( OutOfBoundsException $exception ) {
362 $this->bad_sitemap = true;
363
364 return;
365 }
366
367 $this->sitemap = $this->renderer->get_sitemap( $links, $type, $this->current_page );
368
369 return;
370 }
371
372 if ( has_action( 'wpseo_do_sitemap_' . $type ) ) {
373 /**
374 * Fires custom handler, if hooked to generate sitemap for the type.
375 */
376 do_action( 'wpseo_do_sitemap_' . $type );
377
378 return;
379 }
380
381 $this->bad_sitemap = true;
382 }
383
384 /**
385 * Build the root sitemap (example.com/sitemap_index.xml) which lists sub-sitemaps for other content types.
386 */
387 public function build_root_map() {
388
389 $links = [];
390 $entries_per_page = $this->get_entries_per_page();
391
392 foreach ( $this->providers as $provider ) {
393 $links = array_merge( $links, $provider->get_index_links( $entries_per_page ) );
394 }
395
396 /**
397 * Filter the sitemap links array before the index sitemap is built.
398 *
399 * @param array $links Array of sitemap links
400 */
401 $links = apply_filters( 'wpseo_sitemap_index_links', $links );
402
403 if ( empty( $links ) ) {
404 $this->bad_sitemap = true;
405 $this->sitemap = '';
406
407 return;
408 }
409
410 $this->sitemap = $this->renderer->get_index( $links );
411 }
412
413 /**
414 * Spits out the XSL for the XML sitemap.
415 *
416 * @since 1.4.13
417 *
418 * @param string $type Type to output.
419 */
420 public function xsl_output( $type ) {
421
422 if ( $type !== 'main' ) {
423
424 /**
425 * Fires for the output of XSL for XML sitemaps, other than type "main".
426 */
427 do_action( 'wpseo_xsl_' . $type );
428
429 return;
430 }
431
432 header( $this->http_protocol . ' 200 OK', true, 200 );
433 // Prevent the search engines from indexing the XML Sitemap.
434 header( 'X-Robots-Tag: noindex, follow', true );
435 header( 'Content-Type: text/xml' );
436
437 // Make the browser cache this file properly.
438 $expires = YEAR_IN_SECONDS;
439 header( 'Pragma: public' );
440 header( 'Cache-Control: max-age=' . $expires );
441 header( 'Expires: ' . YoastSEO()->helpers->date->format_timestamp( ( time() + $expires ), 'D, d M Y H:i:s' ) . ' GMT' );
442
443 // Don't use WP_Filesystem() here because that's not initialized yet. See https://yoast.atlassian.net/browse/QAK-2043.
444 readfile( WPSEO_PATH . 'css/main-sitemap.xsl' );
445 }
446
447 /**
448 * Spit out the generated sitemap.
449 */
450 public function output() {
451 $this->send_headers();
452 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping sitemap as either xml or html results in empty document.
453 echo $this->renderer->get_output( $this->sitemap );
454 }
455
456 /**
457 * Makes a request to the sitemap index to cache it before the arrival of the search engines.
458 *
459 * @return void
460 */
461 public function hit_sitemap_index() {
462 if ( ! $this->cache->is_enabled() ) {
463 return;
464 }
465
466 wp_remote_get( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) );
467 }
468
469 /**
470 * Get the GMT modification date for the last modified post in the post type.
471 *
472 * @since 3.2
473 *
474 * @param string|array $post_types Post type or array of types.
475 * @param bool $return_all Flag to return array of values.
476 *
477 * @return string|array|false
478 */
479 public static function get_last_modified_gmt( $post_types, $return_all = false ) {
480
481 global $wpdb;
482
483 static $post_type_dates = null;
484
485 if ( ! is_array( $post_types ) ) {
486 $post_types = [ $post_types ];
487 }
488
489 foreach ( $post_types as $post_type ) {
490 if ( ! isset( $post_type_dates[ $post_type ] ) ) { // If we hadn't seen post type before. R.
491 $post_type_dates = null;
492 break;
493 }
494 }
495
496 if ( is_null( $post_type_dates ) ) {
497
498 $post_type_dates = [];
499 $post_type_names = WPSEO_Post_Type::get_accessible_post_types();
500
501 if ( ! empty( $post_type_names ) ) {
502 $post_statuses = array_map( 'esc_sql', self::get_post_statuses() );
503
504 $sql = "
505 SELECT post_type, MAX(post_modified_gmt) AS date
506 FROM $wpdb->posts
507 WHERE post_status IN ('" . implode( "','", $post_statuses ) . "')
508 AND post_type IN ('" . implode( "','", $post_type_names ) . "')
509 GROUP BY post_type
510 ORDER BY date DESC
511 ";
512
513 foreach ( $wpdb->get_results( $sql ) as $obj ) {
514 $post_type_dates[ $obj->post_type ] = $obj->date;
515 }
516 }
517 }
518
519 $dates = array_intersect_key( $post_type_dates, array_flip( $post_types ) );
520
521 if ( count( $dates ) > 0 ) {
522 if ( $return_all ) {
523 return $dates;
524 }
525
526 return max( $dates );
527 }
528
529 return false;
530 }
531
532 /**
533 * Get the modification date for the last modified post in the post type.
534 *
535 * @param array $post_types Post types to get the last modification date for.
536 *
537 * @return string
538 */
539 public function get_last_modified( $post_types ) {
540 return YoastSEO()->helpers->date->format( self::get_last_modified_gmt( $post_types ) );
541 }
542
543 /**
544 * Notify search engines of the updated sitemap.
545 *
546 * @param string|null $url Optional URL to make the ping for.
547 */
548 public static function ping_search_engines( $url = null ) {
549
550 /**
551 * Filter: 'wpseo_allow_xml_sitemap_ping' - Check if pinging is not allowed (allowed by default)
552 *
553 * @api boolean $allow_ping The boolean that is set to true by default.
554 */
555 if ( apply_filters( 'wpseo_allow_xml_sitemap_ping', true ) === false ) {
556 return;
557 }
558
559 if ( get_option( 'blog_public' ) === '0' ) { // Don't ping if blog is not public.
560 return;
561 }
562
563 if ( empty( $url ) ) {
564 $url = rawurlencode( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) );
565 }
566
567 // Ping Google and Bing.
568 wp_remote_get( 'https://www.google.com/ping?sitemap=' . $url, [ 'blocking' => false ] );
569 wp_remote_get( 'https://www.bing.com/ping?sitemap=' . $url, [ 'blocking' => false ] );
570 }
571
572 /**
573 * Get the maximum number of entries per XML sitemap.
574 *
575 * @return int The maximum number of entries.
576 */
577 protected function get_entries_per_page() {
578 /**
579 * Filter the maximum number of entries per XML sitemap.
580 *
581 * After changing the output of the filter, make sure that you disable and enable the
582 * sitemaps to make sure the value is picked up for the sitemap cache.
583 *
584 * @param int $entries The maximum number of entries per XML sitemap.
585 */
586 $entries = (int) apply_filters( 'wpseo_sitemap_entries_per_page', 1000 );
587
588 return $entries;
589 }
590
591 /**
592 * Get post statuses for post_type or the root sitemap.
593 *
594 * @since 10.2
595 *
596 * @param string $type Provide a type for a post_type sitemap, SITEMAP_INDEX_TYPE for the root sitemap.
597 *
598 * @return array List of post statuses.
599 */
600 public static function get_post_statuses( $type = self::SITEMAP_INDEX_TYPE ) {
601 /**
602 * Filter post status list for sitemap query for the post type.
603 *
604 * @param array $post_statuses Post status list, defaults to array( 'publish' ).
605 * @param string $type Post type or SITEMAP_INDEX_TYPE.
606 */
607 $post_statuses = apply_filters( 'wpseo_sitemap_post_statuses', [ 'publish' ], $type );
608
609 if ( ! is_array( $post_statuses ) || empty( $post_statuses ) ) {
610 $post_statuses = [ 'publish' ];
611 }
612
613 if ( ( $type === self::SITEMAP_INDEX_TYPE || $type === 'attachment' )
614 && ! in_array( 'inherit', $post_statuses, true )
615 ) {
616 $post_statuses[] = 'inherit';
617 }
618
619 return $post_statuses;
620 }
621
622 /**
623 * Sends all the required HTTP Headers.
624 */
625 private function send_headers() {
626 if ( headers_sent() ) {
627 return;
628 }
629
630 $headers = [
631 $this->http_protocol . ' 200 OK' => 200,
632 // Prevent the search engines from indexing the XML Sitemap.
633 'X-Robots-Tag: noindex, follow' => '',
634 'Content-Type: text/xml; charset=' . esc_attr( $this->renderer->get_output_charset() ) => '',
635 ];
636
637 /**
638 * Filter the HTTP headers we send before an XML sitemap.
639 *
640 * @param array $headers The HTTP headers we're going to send out.
641 */
642 $headers = apply_filters( 'wpseo_sitemap_http_headers', $headers );
643
644 foreach ( $headers as $header => $status ) {
645 if ( is_numeric( $status ) ) {
646 header( $header, true, $status );
647 continue;
648 }
649 header( $header, true );
650 }
651 }
652 }
653