Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
34.55% covered (danger)
34.55%
76 / 220
54.55% covered (warning)
54.55%
6 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
jetpack_sitemap_uri
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
Jetpack_Sitemap_Manager
39.18% covered (danger)
39.18%
76 / 194
60.00% covered (warning)
60.00%
6 / 10
310.66
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
5
 serve_raw_and_die
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 callback_action_catch_sitemap_urls
13.08% covered (danger)
13.08%
14 / 107
0.00% covered (danger)
0.00%
0 / 1
206.76
 callback_add_sitemap_schedule
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 callback_sitemap_cron_hook
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 schedule_sitemap_generation
50.00% covered (danger)
50.00%
6 / 12
0.00% covered (danger)
0.00%
0 / 1
2.50
 callback_action_do_robotstxt
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 callback_action_flush_news_sitemap_cache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 callback_action_purge_data
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 callback_action_filter_sitemap_location
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Generate sitemap files in base XML as well as some namespace extensions.
4 *
5 * This module generates two different base sitemaps.
6 *
7 * 1. sitemap.xml
8 *    The basic sitemap is updated regularly by wp-cron. It is stored in the
9 *    database and retrieved when requested. This sitemap aims to include canonical
10 *    URLs for all published content and abide by the sitemap spec. This is the root
11 *    of a tree of sitemap and sitemap index xml files, depending on the number of URLs.
12 *
13 *    By default the sitemap contains published posts of type 'post' and 'page', as
14 *    well as the home url. To include other post types use the 'jetpack_sitemap_post_types'
15 *    filter.
16 *
17 * @link https://www.sitemaps.org/protocol.html Base sitemaps protocol.
18 * @link https://support.google.com/webmasters/answer/178636 Image sitemap extension.
19 * @link https://developers.google.com/webmasters/videosearch/sitemaps Video sitemap extension.
20 *
21 * 2. news-sitemap.xml
22 *    The news sitemap is generated on the fly when requested. It does not aim for
23 *    completeness, instead including at most 1000 of the most recent published posts
24 *    from the previous 2 days, per the news-sitemap spec.
25 *
26 * @link https://support.google.com/webmasters/answer/74288 News sitemap extension.
27 *
28 * @package automattic/jetpack
29 * @since 3.9.0
30 * @since 4.8.0 Remove 1000 post limit.
31 * @author Automattic
32 */
33
34if ( ! defined( 'ABSPATH' ) ) {
35    exit( 0 );
36}
37
38/* Include all of the sitemap subclasses. */
39require_once __DIR__ . '/sitemap-constants.php';
40require_once __DIR__ . '/sitemap-buffer.php';
41require_once __DIR__ . '/sitemap-buffer-fallback.php';
42require_once __DIR__ . '/sitemap-buffer-xmlwriter.php';
43require_once __DIR__ . '/sitemap-buffer-page-xmlwriter.php';
44require_once __DIR__ . '/sitemap-buffer-image-xmlwriter.php';
45require_once __DIR__ . '/sitemap-buffer-video-xmlwriter.php';
46require_once __DIR__ . '/sitemap-buffer-news-xmlwriter.php';
47require_once __DIR__ . '/sitemap-buffer-master-xmlwriter.php';
48require_once __DIR__ . '/sitemap-buffer-factory.php';
49require_once __DIR__ . '/sitemap-stylist.php';
50require_once __DIR__ . '/sitemap-librarian.php';
51require_once __DIR__ . '/sitemap-finder.php';
52require_once __DIR__ . '/sitemap-builder.php';
53
54if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
55    require_once __DIR__ . '/sitemap-logger.php';
56}
57
58// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
59
60/**
61 * Governs the generation, storage, and serving of sitemaps.
62 *
63 * @since 4.8.0
64 *
65 * @phan-constructor-used-for-side-effects
66 */
67class Jetpack_Sitemap_Manager {
68
69    /**
70     * Librarian object for storing and retrieving sitemap data.
71     *
72     * @see Jetpack_Sitemap_Librarian
73     * @since 4.8.0
74     * @var Jetpack_Sitemap_Librarian $librarian Librarian object for storing and retrieving sitemap data.
75     */
76    private $librarian;
77
78    /**
79     * Logger object for reporting debug messages.
80     *
81     * @see Jetpack_Sitemap_Logger
82     * @since 4.8.0
83     * @var Jetpack_Sitemap_Logger $logger Logger object for reporting debug messages.
84     */
85    private $logger;
86
87    /**
88     * Finder object for handling sitemap URIs.
89     *
90     * @see Jetpack_Sitemap_Finder
91     * @since 4.8.0
92     * @var Jetpack_Sitemap_Finder $finder Finder object for handling with sitemap URIs.
93     */
94    private $finder;
95
96    /**
97     * Construct a new Jetpack_Sitemap_Manager.
98     *
99     * @access public
100     * @since 4.8.0
101     */
102    public function __construct() {
103        $this->librarian = new Jetpack_Sitemap_Librarian();
104        $this->finder    = new Jetpack_Sitemap_Finder();
105
106        if ( defined( 'WP_DEBUG' ) && ( true === WP_DEBUG ) ) {
107            $this->logger = new Jetpack_Sitemap_Logger();
108        }
109
110        // Add callback for sitemap URL handler.
111        add_action(
112            'wp_loaded',
113            array( $this, 'callback_action_catch_sitemap_urls' ),
114            defined( 'IS_WPCOM' ) && IS_WPCOM ? 100 : 10
115        );
116
117        // Add generator to wp_cron task list.
118        $this->schedule_sitemap_generation();
119
120        // Add sitemap to robots.txt.
121        add_action(
122            'do_robotstxt',
123            array( $this, 'callback_action_do_robotstxt' ),
124            20
125        );
126
127        // The news sitemap is cached; here we add a callback to
128        // flush the cached news sitemap when a post is published.
129        add_action(
130            'publish_post',
131            array( $this, 'callback_action_flush_news_sitemap_cache' ),
132            10
133        );
134
135        // In case we need to purge all sitemaps, we do this.
136        add_action(
137            'jetpack_sitemaps_purge_data',
138            array( $this, 'callback_action_purge_data' )
139        );
140
141        /*
142         * Module parameters are stored as options in the database.
143         * This allows us to avoid having to process all of init
144         * before serving the sitemap data. The following actions
145         * process and store these filters.
146         */
147
148        // Process filters and store location string for sitemap.
149        add_action(
150            'init',
151            array( $this, 'callback_action_filter_sitemap_location' ),
152            999
153        );
154    }
155
156    /**
157     * Echo a raw string of given content-type.
158     *
159     * @access private
160     * @since 4.8.0
161     *
162     * @param string $the_content_type The content type to be served.
163     * @param string $the_content The string to be echoed.
164     * @return never
165     */
166    private function serve_raw_and_die( $the_content_type, $the_content ) {
167        header( 'Content-Type: ' . $the_content_type . '; charset=UTF-8' );
168
169        global $wp_query;
170        $wp_query->is_feed = true;
171        set_query_var( 'feed', 'sitemap' );
172
173        if ( '' === $the_content ) {
174            $error = __( 'No sitemap found. Please try again later.', 'jetpack' );
175            if ( current_user_can( 'manage_options' ) ) {
176                $next = human_time_diff( wp_next_scheduled( 'jp_sitemap_cron_hook' ) );
177                /* translators: %s is a human_time_diff until next sitemap generation. */
178                $error = sprintf( __( 'No sitemap found. The system will try to build it again in %s.', 'jetpack' ), $next );
179            }
180
181            wp_die(
182                esc_html( $error ),
183                esc_html__( 'Sitemaps', 'jetpack' ),
184                array(
185                    'response' => 404,
186                )
187            );
188        }
189
190        echo $the_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- All content created by Jetpack.
191
192        die( 0 );
193    }
194
195    /**
196     * Callback to intercept sitemap url requests and serve sitemap files.
197     *
198     * @access public
199     * @since 4.8.0
200     */
201    public function callback_action_catch_sitemap_urls() {
202        // Regular expressions for sitemap URL routing.
203        $regex = array(
204            'sitemap'     => '/^sitemap-[1-9][0-9]*\.xml$/',
205            'index'       => '/^sitemap-index-[1-9][0-9]*\.xml$/',
206            'image'       => '/^image-sitemap-[1-9][0-9]*\.xml$/',
207            'image-index' => '/^image-sitemap-index-[1-9][0-9]*\.xml$/',
208            'video'       => '/^video-sitemap-[1-9][0-9]*\.xml$/',
209            'video-index' => '/^video-sitemap-index-[1-9][0-9]*\.xml$/',
210        );
211
212        // The raw path(+query) of the requested URI.
213        if ( isset( $_SERVER['REQUEST_URI'] ) ) { // WPCS: Input var okay.
214            $raw_uri = sanitize_text_field(
215                wp_unslash( $_SERVER['REQUEST_URI'] ) // WPCS: Input var okay.
216            );
217        } else {
218            $raw_uri = '';
219        }
220
221        $request = $this->finder->recognize_sitemap_uri( $raw_uri );
222
223        if ( isset( $request['sitemap_name'] ) ) {
224
225            /**
226             * Filter the content type used to serve the sitemap XML files.
227             *
228             * @module sitemaps
229             *
230             * @since 3.9.0
231             *
232             * @param string $xml_content_type By default, it's 'text/xml'.
233             */
234            $xml_content_type = apply_filters( 'jetpack_sitemap_content_type', 'text/xml' );
235
236            // Catch master sitemap xml.
237            if ( 'sitemap.xml' === $request['sitemap_name'] ) {
238                $sitemap_content = $this->librarian->get_sitemap_text(
239                    jp_sitemap_filename( JP_MASTER_SITEMAP_TYPE, 0 ),
240                    JP_MASTER_SITEMAP_TYPE
241                );
242
243                // if there is no master sitemap yet, let's just return an empty sitemap with a short TTL instead of a 404.
244                if ( empty( $sitemap_content ) ) {
245                    $builder         = new Jetpack_Sitemap_Builder();
246                    $sitemap_content = $builder->empty_sitemap_xml();
247                }
248
249                $this->serve_raw_and_die(
250                    $xml_content_type,
251                    $sitemap_content
252                );
253            }
254
255            // Catch sitemap xsl.
256            if ( 'sitemap.xsl' === $request['sitemap_name'] ) {
257                $this->serve_raw_and_die(
258                    'application/xml',
259                    Jetpack_Sitemap_Stylist::sitemap_xsl()
260                );
261            }
262
263            // Catch sitemap index xsl.
264            if ( 'sitemap-index.xsl' === $request['sitemap_name'] ) {
265                $this->serve_raw_and_die(
266                    'application/xml',
267                    Jetpack_Sitemap_Stylist::sitemap_index_xsl()
268                );
269            }
270
271            // Catch image sitemap xsl.
272            if ( 'image-sitemap.xsl' === $request['sitemap_name'] ) {
273                $this->serve_raw_and_die(
274                    'application/xml',
275                    Jetpack_Sitemap_Stylist::image_sitemap_xsl()
276                );
277            }
278
279            // Catch video sitemap xsl.
280            if ( 'video-sitemap.xsl' === $request['sitemap_name'] ) {
281                $this->serve_raw_and_die(
282                    'application/xml',
283                    Jetpack_Sitemap_Stylist::video_sitemap_xsl()
284                );
285            }
286
287            // Catch news sitemap xml.
288            if ( 'news-sitemap.xml' === $request['sitemap_name'] ) {
289                $sitemap_builder = new Jetpack_Sitemap_Builder();
290                $this->serve_raw_and_die(
291                    $xml_content_type,
292                    $sitemap_builder->news_sitemap_xml()
293                );
294            }
295
296            // Catch news sitemap xsl.
297            if ( 'news-sitemap.xsl' === $request['sitemap_name'] ) {
298                $this->serve_raw_and_die(
299                    'application/xml',
300                    Jetpack_Sitemap_Stylist::news_sitemap_xsl()
301                );
302            }
303
304            // Catch sitemap xml.
305            if ( preg_match( $regex['sitemap'], $request['sitemap_name'] ) ) {
306                $this->serve_raw_and_die(
307                    $xml_content_type,
308                    $this->librarian->get_sitemap_text(
309                        $request['sitemap_name'],
310                        JP_PAGE_SITEMAP_TYPE
311                    )
312                );
313            }
314
315            // Catch sitemap index xml.
316            if ( preg_match( $regex['index'], $request['sitemap_name'] ) ) {
317                $this->serve_raw_and_die(
318                    $xml_content_type,
319                    $this->librarian->get_sitemap_text(
320                        $request['sitemap_name'],
321                        JP_PAGE_SITEMAP_INDEX_TYPE
322                    )
323                );
324            }
325
326            // Catch image sitemap xml.
327            if ( preg_match( $regex['image'], $request['sitemap_name'] ) ) {
328                $this->serve_raw_and_die(
329                    $xml_content_type,
330                    $this->librarian->get_sitemap_text(
331                        $request['sitemap_name'],
332                        JP_IMAGE_SITEMAP_TYPE
333                    )
334                );
335            }
336
337            // Catch image sitemap index xml.
338            if ( preg_match( $regex['image-index'], $request['sitemap_name'] ) ) {
339                $this->serve_raw_and_die(
340                    $xml_content_type,
341                    $this->librarian->get_sitemap_text(
342                        $request['sitemap_name'],
343                        JP_IMAGE_SITEMAP_INDEX_TYPE
344                    )
345                );
346            }
347
348            // Catch video sitemap xml.
349            if ( preg_match( $regex['video'], $request['sitemap_name'] ) ) {
350                $this->serve_raw_and_die(
351                    $xml_content_type,
352                    $this->librarian->get_sitemap_text(
353                        $request['sitemap_name'],
354                        JP_VIDEO_SITEMAP_TYPE
355                    )
356                );
357            }
358
359            // Catch video sitemap index xml.
360            if ( preg_match( $regex['video-index'], $request['sitemap_name'] ) ) {
361                $this->serve_raw_and_die(
362                    $xml_content_type,
363                    $this->librarian->get_sitemap_text(
364                        $request['sitemap_name'],
365                        JP_VIDEO_SITEMAP_INDEX_TYPE
366                    )
367                );
368            }
369        }
370    }
371
372    /**
373     * Callback for adding sitemap-interval to the list of schedules.
374     *
375     * @access public
376     * @since 4.8.0
377     *
378     * @param array $schedules The array of WP_Cron schedules.
379     *
380     * @return array The updated array of WP_Cron schedules.
381     */
382    public function callback_add_sitemap_schedule( $schedules ) {
383        $schedules['sitemap-interval'] = array(
384            'interval' => JP_SITEMAP_INTERVAL,
385            'display'  => __( 'Sitemap Interval', 'jetpack' ),
386        );
387        return $schedules;
388    }
389
390    /**
391     * Callback handler for sitemap cron hook
392     *
393     * @access public
394     */
395    public function callback_sitemap_cron_hook() {
396        $sitemap_builder = new Jetpack_Sitemap_Builder();
397        $sitemap_builder->update_sitemap();
398    }
399
400    /**
401     * Add actions to schedule sitemap generation.
402     * Should only be called once, in the constructor.
403     *
404     * @access private
405     * @since 4.8.0
406     */
407    private function schedule_sitemap_generation() {
408        // Add cron schedule.
409        add_filter( 'cron_schedules', array( $this, 'callback_add_sitemap_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
410
411        add_action(
412            'jp_sitemap_cron_hook',
413            array( $this, 'callback_sitemap_cron_hook' )
414        );
415
416        if ( ! wp_next_scheduled( 'jp_sitemap_cron_hook' ) ) {
417            /**
418             * Filter the delay in seconds until sitemap generation cron job is started.
419             *
420             * This filter allows a site operator or hosting provider to potentialy spread out sitemap generation for a
421             * lot of sites over time. By default, it will be randomly done over 15 minutes.
422             *
423             * @module sitemaps
424             * @since 6.6.1
425             *
426             * @param int $delay Time to delay in seconds.
427             */
428            $delay = apply_filters( 'jetpack_sitemap_generation_delay', MINUTE_IN_SECONDS * wp_rand( 1, 15 ) ); // Randomly space it out to start within next fifteen minutes.
429            wp_schedule_event(
430                time() + $delay,
431                'sitemap-interval',
432                'jp_sitemap_cron_hook'
433            );
434        }
435    }
436
437    /**
438     * Callback to add sitemap to robots.txt.
439     *
440     * @access public
441     * @since 4.8.0
442     */
443    public function callback_action_do_robotstxt() {
444
445        /**
446         * Filter whether to make the default sitemap discoverable to robots or not. Default true.
447         *
448         * @module sitemaps
449         * @since 3.9.0
450         * @deprecated 7.4.0
451         *
452         * @param bool $discover_sitemap Make default sitemap discoverable to robots.
453         */
454        $discover_sitemap = apply_filters_deprecated( 'jetpack_sitemap_generate', array( true ), 'jetpack-7.4.0', 'jetpack_sitemap_include_in_robotstxt' );
455
456        /**
457         * Filter whether to make the default sitemap discoverable to robots or not. Default true.
458         *
459         * @module sitemaps
460         * @since 7.4.0
461         *
462         * @param bool $discover_sitemap Make default sitemap discoverable to robots.
463         */
464        $discover_sitemap = apply_filters( 'jetpack_sitemap_include_in_robotstxt', $discover_sitemap );
465
466        if ( true === $discover_sitemap ) {
467            $sitemap_url = $this->finder->construct_sitemap_url( 'sitemap.xml' );
468            echo 'Sitemap: ' . esc_url( $sitemap_url ) . "\n";
469        }
470
471        /**
472         * Filter whether to make the news sitemap discoverable to robots or not. Default true.
473         *
474         * @module sitemaps
475         * @since 3.9.0
476         * @deprecated 7.4.0
477         *
478         * @param bool $discover_news_sitemap Make default news sitemap discoverable to robots.
479         */
480        $discover_news_sitemap = apply_filters_deprecated( 'jetpack_news_sitemap_generate', array( true ), 'jetpack-7.4.0', 'jetpack_news_sitemap_include_in_robotstxt' );
481
482        /**
483         * Filter whether to make the news sitemap discoverable to robots or not. Default true.
484         *
485         * @module sitemaps
486         * @since 7.4.0
487         *
488         * @param bool $discover_news_sitemap Make default news sitemap discoverable to robots.
489         */
490        $discover_news_sitemap = apply_filters( 'jetpack_news_sitemap_include_in_robotstxt', $discover_news_sitemap );
491
492        if ( true === $discover_news_sitemap ) {
493            $news_sitemap_url = $this->finder->construct_sitemap_url( 'news-sitemap.xml' );
494            echo 'Sitemap: ' . esc_url( $news_sitemap_url ) . "\n";
495        }
496    }
497
498    /**
499     * Callback to delete the news sitemap cache.
500     *
501     * @access public
502     * @since 4.8.0
503     */
504    public function callback_action_flush_news_sitemap_cache() {
505        delete_transient( 'jetpack_news_sitemap_xml' );
506    }
507
508    /**
509     * Callback for resetting stored sitemap data.
510     *
511     * @access public
512     * @since 5.3.0
513     * @since 6.7.0 Schedules a regeneration.
514     */
515    public function callback_action_purge_data() {
516        $this->callback_action_flush_news_sitemap_cache();
517        $this->librarian->delete_all_stored_sitemap_data();
518        /** This filter is documented in modules/sitemaps/sitemaps.php */
519        $delay = apply_filters( 'jetpack_sitemap_generation_delay', MINUTE_IN_SECONDS * wp_rand( 1, 15 ) ); // Randomly space it out to start within next fifteen minutes.
520        wp_schedule_single_event( time() + $delay, 'jp_sitemap_cron_hook' );
521    }
522
523    /**
524     * Callback to set the sitemap location.
525     *
526     * @access public
527     * @since 4.8.0
528     */
529    public function callback_action_filter_sitemap_location() {
530        update_option(
531            'jetpack_sitemap_location',
532            /**
533             * Additional path for sitemap URIs. Default value is empty.
534             *
535             * This string is any additional path fragment you want included between
536             * the home URL and the sitemap filenames. Exactly how this fragment is
537             * interpreted depends on your permalink settings. For example:
538             *
539             *   Pretty permalinks:
540             *     home_url() . jetpack_sitemap_location . '/sitemap.xml'
541             *
542             *   Plain ("ugly") permalinks:
543             *     home_url() . jetpack_sitemap_location . '/?jetpack-sitemap=sitemap.xml'
544             *
545             *   PATHINFO permalinks:
546             *     home_url() . '/index.php' . jetpack_sitemap_location . '/sitemap.xml'
547             *
548             * where 'sitemap.xml' is the name of a specific sitemap file.
549             * The value of this filter must be a valid path fragment per RFC 3986;
550             * in particular it must either be empty or begin with a '/'.
551             * Also take care that any restrictions on sitemap location imposed by
552             * the sitemap protocol are satisfied.
553             *
554             * The result of this filter is stored in an option, 'jetpack_sitemap_location';
555             * that option is what gets read when the sitemap location is needed.
556             * This way we don't have to wait for init to finish before building sitemaps.
557             *
558             * @link https://tools.ietf.org/html/rfc3986#section-3.3 RFC 3986
559             * @link https://www.sitemaps.org/ The sitemap protocol
560             *
561             * @since 4.8.0
562             */
563            apply_filters(
564                'jetpack_sitemap_location',
565                ''
566            )
567        );
568    }
569} // End Jetpack_Sitemap_Manager class.
570
571new Jetpack_Sitemap_Manager();
572
573/**
574 * Absolute URL of the current blog's sitemap.
575 *
576 * @module sitemaps
577 *
578 * @since  3.9.0
579 * @since  4.8.1 Code uses method found in Jetpack_Sitemap_Finder::construct_sitemap_url in 4.8.0.
580 *                It has been moved here to avoid fatal errors with other plugins that were expecting to find this function.
581 *
582 * @param string $filename Sitemap file name. Defaults to 'sitemap.xml', the initial sitemaps page.
583 *
584 * @return string Sitemap URL.
585 */
586function jetpack_sitemap_uri( $filename = 'sitemap.xml' ) {
587    global $wp_rewrite;
588
589    $location = Jetpack_Options::get_option_and_ensure_autoload( 'jetpack_sitemap_location', '' );
590
591    if ( $wp_rewrite->using_index_permalinks() ) {
592        $sitemap_url = home_url( '/index.php' . $location . '/' . $filename );
593    } elseif ( $wp_rewrite->using_permalinks() ) {
594        $sitemap_url = home_url( $location . '/' . $filename );
595    } else {
596        $sitemap_url = home_url( $location . '/?jetpack-sitemap=' . $filename );
597    }
598
599    /**
600     * Filter sitemap URL relative to home URL.
601     *
602     * @module sitemaps
603     *
604     * @since 3.9.0
605     *
606     * @param string $sitemap_url Sitemap URL.
607     */
608    return apply_filters( 'jetpack_sitemap_location', $sitemap_url );
609}