Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
51.57% covered (warning)
51.57%
164 / 318
66.67% covered (warning)
66.67%
14 / 21
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
56.55% covered (warning)
56.55%
164 / 290
70.00% covered (warning)
70.00%
14 / 20
648.03
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
5
 is_sitemap_ready
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 callback_redirect_core_index
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 callback_filter_core_robots
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 callback_filter_core_query
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
4
 callback_filter_core_posts
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 callback_invalidate_index_cache
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
13
 callback_sitemap_saved
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
9
 callback_purge_pending_index_cache
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
6.07
 callback_sitemap_deleted
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 prevent_caching
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 serve_raw_and_die
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 callback_action_catch_sitemap_urls
12.61% covered (danger)
12.61%
14 / 111
0.00% covered (danger)
0.00%
0 / 1
234.22
 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
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 callback_action_do_robotstxt
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 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_action( 'template_redirect', array( $this, 'callback_redirect_core_index' ), 0 );
118        add_filter( 'robots_txt', array( $this, 'callback_filter_core_robots' ), 1 );
119        add_filter( 'wp_sitemaps_posts_query_args', array( $this, 'callback_filter_core_query' ) );
120        add_filter( 'the_posts', array( $this, 'callback_filter_core_posts' ), 10, 2 );
121        add_action( 'wp_loaded', array( __CLASS__, 'callback_purge_pending_index_cache' ), 20 );
122        add_action( 'wp_after_insert_post', array( __CLASS__, 'callback_sitemap_saved' ), 10, 4 );
123        add_action( 'jetpack_activate_module_sitemaps', array( __CLASS__, 'callback_invalidate_index_cache' ) );
124        add_action( 'jetpack_deactivate_module_sitemaps', array( __CLASS__, 'callback_invalidate_index_cache' ) );
125        add_action( 'deleted_post', array( __CLASS__, 'callback_sitemap_deleted' ), 10, 2 );
126
127        // Add generator to wp_cron task list.
128        $this->schedule_sitemap_generation();
129
130        // Add sitemap to robots.txt.
131        add_action(
132            'do_robotstxt',
133            array( $this, 'callback_action_do_robotstxt' ),
134            20
135        );
136
137        // The news sitemap is cached; here we add a callback to
138        // flush the cached news sitemap when a post is published.
139        add_action(
140            'publish_post',
141            array( $this, 'callback_action_flush_news_sitemap_cache' ),
142            10
143        );
144
145        // In case we need to purge all sitemaps, we do this.
146        add_action(
147            'jetpack_sitemaps_purge_data',
148            array( $this, 'callback_action_purge_data' )
149        );
150
151        /*
152         * Module parameters are stored as options in the database.
153         * This allows us to avoid having to process all of init
154         * before serving the sitemap data. The following actions
155         * process and store these filters.
156         */
157
158        // Process filters and store location string for sitemap.
159        add_action(
160            'init',
161            array( $this, 'callback_action_filter_sitemap_location' ),
162            999
163        );
164    }
165
166    /**
167     * Whether Jetpack has stored a generated master index.
168     *
169     * @since $$next-version$$
170     * @return bool
171     */
172    public function is_sitemap_ready() {
173        return '' !== $this->librarian->get_sitemap_text(
174            jp_sitemap_filename( JP_MASTER_SITEMAP_TYPE, '0' ),
175            JP_MASTER_SITEMAP_TYPE
176        );
177    }
178
179    /**
180     * Redirect submitted core indexes without breaking their child sitemap URLs.
181     *
182     * @since $$next-version$$
183     */
184    public function callback_redirect_core_index() {
185        if ( 'index' !== get_query_var( 'sitemap' ) || ! $this->is_sitemap_ready() || ! wp_sitemaps_get_server()->sitemaps_enabled() ) {
186            return;
187        }
188
189        $this->prevent_caching();
190        wp_safe_redirect( jetpack_sitemap_uri(), 301, 'Jetpack' );
191        exit;
192    }
193
194    /**
195     * Advertise only Jetpack's index once it is available.
196     *
197     * @since $$next-version$$
198     * @param string $output Robots.txt contents.
199     * @return string
200     */
201    public function callback_filter_core_robots( $output ) {
202        if ( ! $this->is_sitemap_ready() ) {
203            return $output;
204        }
205
206        $core_line = 'Sitemap: ' . esc_url( get_sitemap_url( 'index' ) );
207        return preg_replace( '/^' . preg_quote( $core_line, '/' ) . '\r?(?:\n|$)/m', '', $output );
208    }
209
210    /**
211     * Apply SEO exclusions to core's fallback queries, including their page counts.
212     *
213     * @since $$next-version$$
214     * @param array $args Core sitemap query arguments.
215     * @return array
216     */
217    public function callback_filter_core_query( $args ) {
218        $args['jetpack_sitemap_fallback'] = true;
219        if ( has_filter( 'jetpack_sitemap_skip_post', array( 'Jetpack_SEO_Posts', 'exclude_noindex_posts_from_jetpack_sitemap' ) ) && Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
220            $indexable          = array(
221                'relation'         => 'OR',
222                'noindex_missing'  => array(
223                    'key'     => 'jetpack_seo_noindex',
224                    'compare' => 'NOT EXISTS',
225                ),
226                'noindex_disabled' => array(
227                    'key'     => 'jetpack_seo_noindex',
228                    'value'   => array( '', '0' ),
229                    'compare' => 'IN',
230                ),
231            );
232            $args['meta_query'] = empty( $args['meta_query'] ) ? $indexable : array(
233                'relation'          => 'AND',
234                'existing'          => $args['meta_query'],
235                'jetpack_indexable' => $indexable,
236            ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Scoped sitemap queries must exclude noindex posts before pagination.
237        }
238        return $args;
239    }
240
241    /**
242     * Preserve custom Jetpack post exclusions on core's fallback sitemap pages.
243     *
244     * @since $$next-version$$
245     * @param WP_Post[] $posts Query results.
246     * @param WP_Query  $query Current query.
247     * @return WP_Post[]
248     */
249    public function callback_filter_core_posts( $posts, $query ) {
250        if ( ! $query->get( 'jetpack_sitemap_fallback' ) ) {
251            return $posts;
252        }
253        update_meta_cache( 'post', wp_list_pluck( $posts, 'ID' ) );
254        return array_values(
255            array_filter(
256                $posts,
257                static function ( $post ) {
258                    return true !== apply_filters( 'jetpack_sitemap_skip_post', false, $post );
259                }
260            )
261        );
262    }
263
264    /**
265     * Invalidate only the index and discovery URLs after a stored index changes.
266     *
267     * @since $$next-version$$
268     * @return string[] Discovery URLs supplied to cache adapters.
269     */
270    public static function callback_invalidate_index_cache() {
271        $url       = jetpack_sitemap_uri();
272        $urls      = array_unique( array( $url, get_sitemap_url( 'index' ), home_url( '/robots.txt' ) ) );
273        $clear_url = function_exists( 'batcache_clear_url' ) ? 'batcache_clear_url' : null;
274        if ( ! $clear_url && function_exists( 'pcm_flush_batcache_url' ) ) {
275            $clear_url = 'pcm_flush_batcache_url';
276        }
277        if ( $clear_url && defined( 'WP_CLI' ) && WP_CLI && ! get_option( 'jetpack_sitemap_pending_cache_purge' ) ) {
278            update_option( 'jetpack_sitemap_pending_cache_purge', 'urls', false );
279        }
280        if ( $clear_url && ! ( defined( 'WP_CLI' ) && WP_CLI ) ) {
281            // Batcache shares a URL version across query variants, except for the "p" parameter.
282            $cache_urls = array();
283            foreach ( $urls as $cached_url ) {
284                $query = array();
285                wp_parse_str( (string) wp_parse_url( $cached_url, PHP_URL_QUERY ), $query );
286                $cache_urls[] = remove_query_arg( array_diff( array_keys( $query ), array( 'p' ) ), $cached_url );
287            }
288            foreach ( array_unique( $cache_urls ) as $cached_url ) {
289                $clear_url( $cached_url );
290            }
291        }
292
293        /**
294         * Invalidate sitemap discovery responses in additional page caches.
295         *
296         * @since $$next-version$$
297         * @param string   $url  Jetpack sitemap index URL.
298         * @param string[] $urls Sitemap index and discovery URLs to invalidate.
299         */
300        do_action( 'jetpack_sitemap_index_updated', $url, $urls );
301        return $urls;
302    }
303
304    /**
305     * Purge cached pending responses when the first usable index is stored.
306     *
307     * @since $$next-version$$
308     * @param int           $post_id Stored post ID.
309     * @param \WP_Post|null $post Stored sitemap post.
310     * @param bool          $update Whether this updates an existing post.
311     * @param \WP_Post|null $post_before Previously stored post, if any.
312     */
313    public static function callback_sitemap_saved( $post_id, $post, $update, $post_before ) {
314        if ( ! $post instanceof \WP_Post || JP_MASTER_SITEMAP_TYPE !== $post->post_type || jp_sitemap_filename( JP_MASTER_SITEMAP_TYPE, '0' ) !== $post->post_title ) {
315            return;
316        }
317
318        if ( '' !== $post->post_content && ( ! $post_before || '' === $post_before->post_content ) ) {
319            update_option( 'jetpack_sitemap_pending_cache_purge', 'first-build', false );
320            if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) {
321                self::callback_purge_pending_index_cache();
322                return;
323            }
324        }
325        self::callback_invalidate_index_cache();
326    }
327
328    /**
329     * Finish deferred cache recovery in a web bootstrap with host cache APIs.
330     *
331     * @since $$next-version$$
332     */
333    public static function callback_purge_pending_index_cache() {
334        $pending = get_option( 'jetpack_sitemap_pending_cache_purge' );
335        if ( ( defined( 'WP_CLI' ) && WP_CLI ) || ! $pending ) {
336            return;
337        }
338        if ( ! delete_option( 'jetpack_sitemap_pending_cache_purge' ) ) {
339            return;
340        }
341        $urls = self::callback_invalidate_index_cache();
342        if ( 'first-build' === $pending ) {
343            // Some hosts also flush the whole origin; avoid doing that on routine rebuilds.
344            do_action( 'edge_cache_purge_uris', $urls );
345        }
346    }
347
348    /**
349     * Invalidate discovery responses when the generated index is removed.
350     *
351     * @since $$next-version$$
352     * @param int           $post_id Deleted post ID.
353     * @param \WP_Post|null $post Deleted post.
354     */
355    public static function callback_sitemap_deleted( $post_id, $post ) {
356        if ( $post instanceof \WP_Post && JP_MASTER_SITEMAP_TYPE === $post->post_type ) {
357            self::callback_invalidate_index_cache();
358        }
359    }
360
361    /**
362     * Keep pending sitemap responses and reversible redirects out of page caches.
363     *
364     * @since $$next-version$$
365     */
366    private function prevent_caching() {
367        nocache_headers();
368        if ( function_exists( 'batcache_cancel' ) ) {
369            // @phan-suppress-next-line PhanUndeclaredFunction -- Optional Batcache API, guarded above.
370            batcache_cancel();
371        }
372    }
373
374    /**
375     * Echo a raw string of given content-type.
376     *
377     * @access private
378     * @since 4.8.0
379     *
380     * @param string $the_content_type The content type to be served.
381     * @param string $the_content The string to be echoed.
382     * @return never
383     */
384    private function serve_raw_and_die( $the_content_type, $the_content ) {
385        header( 'Content-Type: ' . $the_content_type . '; charset=UTF-8' );
386
387        global $wp_query;
388        $wp_query->is_feed = true;
389        set_query_var( 'feed', 'sitemap' );
390
391        if ( '' === $the_content ) {
392            $this->prevent_caching();
393            $error = __( 'No sitemap found. Please try again later.', 'jetpack' );
394            if ( current_user_can( 'manage_options' ) ) {
395                $next = human_time_diff( wp_next_scheduled( 'jp_sitemap_cron_hook' ) );
396                /* translators: %s is a human_time_diff until next sitemap generation. */
397                $error = sprintf( __( 'No sitemap found. The system will try to build it again in %s.', 'jetpack' ), $next );
398            }
399
400            wp_die(
401                esc_html( $error ),
402                esc_html__( 'Sitemaps', 'jetpack' ),
403                array(
404                    'response' => 404,
405                )
406            );
407        }
408
409        echo $the_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- All content created by Jetpack.
410
411        die( 0 );
412    }
413
414    /**
415     * Callback to intercept sitemap url requests and serve sitemap files.
416     *
417     * @access public
418     * @since 4.8.0
419     */
420    public function callback_action_catch_sitemap_urls() {
421        // Regular expressions for sitemap URL routing.
422        $regex = array(
423            'sitemap'     => '/^sitemap-[1-9][0-9]*\.xml$/',
424            'index'       => '/^sitemap-index-[1-9][0-9]*\.xml$/',
425            'image'       => '/^image-sitemap-[1-9][0-9]*\.xml$/',
426            'image-index' => '/^image-sitemap-index-[1-9][0-9]*\.xml$/',
427            'video'       => '/^video-sitemap-[1-9][0-9]*\.xml$/',
428            'video-index' => '/^video-sitemap-index-[1-9][0-9]*\.xml$/',
429        );
430
431        // The raw path(+query) of the requested URI.
432        if ( isset( $_SERVER['REQUEST_URI'] ) ) { // WPCS: Input var okay.
433            $raw_uri = sanitize_text_field(
434                wp_unslash( $_SERVER['REQUEST_URI'] ) // WPCS: Input var okay.
435            );
436        } else {
437            $raw_uri = '';
438        }
439
440        $request = $this->finder->recognize_sitemap_uri( $raw_uri );
441
442        if ( isset( $request['sitemap_name'] ) ) {
443
444            /**
445             * Filter the content type used to serve the sitemap XML files.
446             *
447             * @module sitemaps
448             *
449             * @since 3.9.0
450             *
451             * @param string $xml_content_type By default, it's 'text/xml'.
452             */
453            $xml_content_type = apply_filters( 'jetpack_sitemap_content_type', 'text/xml' );
454
455            // Catch master sitemap xml.
456            if ( 'sitemap.xml' === $request['sitemap_name'] ) {
457                $sitemap_content = $this->librarian->get_sitemap_text(
458                    jp_sitemap_filename( JP_MASTER_SITEMAP_TYPE, '0' ),
459                    JP_MASTER_SITEMAP_TYPE
460                );
461
462                if ( empty( $sitemap_content ) ) {
463                    $this->prevent_caching();
464                    if ( wp_sitemaps_get_server()->sitemaps_enabled() ) {
465                        wp_safe_redirect( get_sitemap_url( 'index' ), 302, 'Jetpack' );
466                        exit;
467                    }
468                    $builder         = new Jetpack_Sitemap_Builder();
469                    $sitemap_content = $builder->empty_sitemap_xml();
470                }
471
472                $this->serve_raw_and_die(
473                    $xml_content_type,
474                    $sitemap_content
475                );
476            }
477
478            // Catch sitemap xsl.
479            if ( 'sitemap.xsl' === $request['sitemap_name'] ) {
480                $this->serve_raw_and_die(
481                    'application/xml',
482                    Jetpack_Sitemap_Stylist::sitemap_xsl()
483                );
484            }
485
486            // Catch sitemap index xsl.
487            if ( 'sitemap-index.xsl' === $request['sitemap_name'] ) {
488                $this->serve_raw_and_die(
489                    'application/xml',
490                    Jetpack_Sitemap_Stylist::sitemap_index_xsl()
491                );
492            }
493
494            // Catch image sitemap xsl.
495            if ( 'image-sitemap.xsl' === $request['sitemap_name'] ) {
496                $this->serve_raw_and_die(
497                    'application/xml',
498                    Jetpack_Sitemap_Stylist::image_sitemap_xsl()
499                );
500            }
501
502            // Catch video sitemap xsl.
503            if ( 'video-sitemap.xsl' === $request['sitemap_name'] ) {
504                $this->serve_raw_and_die(
505                    'application/xml',
506                    Jetpack_Sitemap_Stylist::video_sitemap_xsl()
507                );
508            }
509
510            // Catch news sitemap xml.
511            if ( 'news-sitemap.xml' === $request['sitemap_name'] ) {
512                $sitemap_builder = new Jetpack_Sitemap_Builder();
513                $this->serve_raw_and_die(
514                    $xml_content_type,
515                    $sitemap_builder->news_sitemap_xml()
516                );
517            }
518
519            // Catch news sitemap xsl.
520            if ( 'news-sitemap.xsl' === $request['sitemap_name'] ) {
521                $this->serve_raw_and_die(
522                    'application/xml',
523                    Jetpack_Sitemap_Stylist::news_sitemap_xsl()
524                );
525            }
526
527            // Catch sitemap xml.
528            if ( preg_match( $regex['sitemap'], $request['sitemap_name'] ) ) {
529                $this->serve_raw_and_die(
530                    $xml_content_type,
531                    $this->librarian->get_sitemap_text(
532                        $request['sitemap_name'],
533                        JP_PAGE_SITEMAP_TYPE
534                    )
535                );
536            }
537
538            // Catch sitemap index xml.
539            if ( preg_match( $regex['index'], $request['sitemap_name'] ) ) {
540                $this->serve_raw_and_die(
541                    $xml_content_type,
542                    $this->librarian->get_sitemap_text(
543                        $request['sitemap_name'],
544                        JP_PAGE_SITEMAP_INDEX_TYPE
545                    )
546                );
547            }
548
549            // Catch image sitemap xml.
550            if ( preg_match( $regex['image'], $request['sitemap_name'] ) ) {
551                $this->serve_raw_and_die(
552                    $xml_content_type,
553                    $this->librarian->get_sitemap_text(
554                        $request['sitemap_name'],
555                        JP_IMAGE_SITEMAP_TYPE
556                    )
557                );
558            }
559
560            // Catch image sitemap index xml.
561            if ( preg_match( $regex['image-index'], $request['sitemap_name'] ) ) {
562                $this->serve_raw_and_die(
563                    $xml_content_type,
564                    $this->librarian->get_sitemap_text(
565                        $request['sitemap_name'],
566                        JP_IMAGE_SITEMAP_INDEX_TYPE
567                    )
568                );
569            }
570
571            // Catch video sitemap xml.
572            if ( preg_match( $regex['video'], $request['sitemap_name'] ) ) {
573                $this->serve_raw_and_die(
574                    $xml_content_type,
575                    $this->librarian->get_sitemap_text(
576                        $request['sitemap_name'],
577                        JP_VIDEO_SITEMAP_TYPE
578                    )
579                );
580            }
581
582            // Catch video sitemap index xml.
583            if ( preg_match( $regex['video-index'], $request['sitemap_name'] ) ) {
584                $this->serve_raw_and_die(
585                    $xml_content_type,
586                    $this->librarian->get_sitemap_text(
587                        $request['sitemap_name'],
588                        JP_VIDEO_SITEMAP_INDEX_TYPE
589                    )
590                );
591            }
592        }
593    }
594
595    /**
596     * Callback for adding sitemap-interval to the list of schedules.
597     *
598     * @access public
599     * @since 4.8.0
600     *
601     * @param array $schedules The array of WP_Cron schedules.
602     *
603     * @return array The updated array of WP_Cron schedules.
604     */
605    public function callback_add_sitemap_schedule( $schedules ) {
606        $schedules['sitemap-interval'] = array(
607            'interval' => JP_SITEMAP_INTERVAL,
608            'display'  => __( 'Sitemap Interval', 'jetpack' ),
609        );
610        return $schedules;
611    }
612
613    /**
614     * Callback handler for sitemap cron hook
615     *
616     * @access public
617     */
618    public function callback_sitemap_cron_hook() {
619        $sitemap_builder = new Jetpack_Sitemap_Builder();
620        $sitemap_builder->update_sitemap();
621    }
622
623    /**
624     * Add actions to schedule sitemap generation.
625     * Should only be called once, in the constructor.
626     *
627     * @access private
628     * @since 4.8.0
629     */
630    private function schedule_sitemap_generation() {
631        // Add cron schedule.
632        add_filter( 'cron_schedules', array( $this, 'callback_add_sitemap_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
633
634        add_action(
635            'jp_sitemap_cron_hook',
636            array( $this, 'callback_sitemap_cron_hook' )
637        );
638
639        if ( ! wp_next_scheduled( 'jp_sitemap_cron_hook' ) ) {
640            /**
641             * Filter the delay in seconds until sitemap generation cron job is started.
642             *
643             * This filter allows a site operator or hosting provider to potentialy spread out sitemap generation for a
644             * lot of sites over time. By default, it will be randomly done over 15 minutes.
645             *
646             * @module sitemaps
647             * @since 6.6.1
648             *
649             * @param int $delay Time to delay in seconds.
650             */
651            $delay = apply_filters( 'jetpack_sitemap_generation_delay', MINUTE_IN_SECONDS * wp_rand( 1, 15 ) ); // Randomly space it out to start within next fifteen minutes.
652            wp_schedule_event(
653                time() + $delay,
654                'sitemap-interval',
655                'jp_sitemap_cron_hook'
656            );
657        }
658    }
659
660    /**
661     * Callback to add sitemap to robots.txt.
662     *
663     * @access public
664     * @since 4.8.0
665     */
666    public function callback_action_do_robotstxt() {
667
668        /**
669         * Filter whether to make the default sitemap discoverable to robots or not. Default true.
670         *
671         * @module sitemaps
672         * @since 3.9.0
673         * @deprecated 7.4.0
674         *
675         * @param bool $discover_sitemap Make default sitemap discoverable to robots.
676         */
677        $discover_sitemap = apply_filters_deprecated( 'jetpack_sitemap_generate', array( true ), 'jetpack-7.4.0', 'jetpack_sitemap_include_in_robotstxt' );
678
679        /**
680         * Filter whether to make the default sitemap discoverable to robots or not. Default true.
681         *
682         * @module sitemaps
683         * @since 7.4.0
684         *
685         * @param bool $discover_sitemap Make default sitemap discoverable to robots.
686         */
687        $discover_sitemap = apply_filters( 'jetpack_sitemap_include_in_robotstxt', $discover_sitemap );
688
689        if ( true === $discover_sitemap && $this->is_sitemap_ready() ) {
690            $sitemap_url = $this->finder->construct_sitemap_url( 'sitemap.xml' );
691            echo 'Sitemap: ' . esc_url( $sitemap_url ) . "\n";
692        }
693
694        /**
695         * Filter whether to make the news sitemap discoverable to robots or not. Default true.
696         *
697         * @module sitemaps
698         * @since 3.9.0
699         * @deprecated 7.4.0
700         *
701         * @param bool $discover_news_sitemap Make default news sitemap discoverable to robots.
702         */
703        $discover_news_sitemap = apply_filters_deprecated( 'jetpack_news_sitemap_generate', array( true ), 'jetpack-7.4.0', 'jetpack_news_sitemap_include_in_robotstxt' );
704
705        /**
706         * Filter whether to make the news sitemap discoverable to robots or not. Default true.
707         *
708         * @module sitemaps
709         * @since 7.4.0
710         *
711         * @param bool $discover_news_sitemap Make default news sitemap discoverable to robots.
712         */
713        $discover_news_sitemap = apply_filters( 'jetpack_news_sitemap_include_in_robotstxt', $discover_news_sitemap );
714
715        if ( true === $discover_news_sitemap ) {
716            $news_sitemap_url = $this->finder->construct_sitemap_url( 'news-sitemap.xml' );
717            echo 'Sitemap: ' . esc_url( $news_sitemap_url ) . "\n";
718        }
719    }
720
721    /**
722     * Callback to delete the news sitemap cache.
723     *
724     * @access public
725     * @since 4.8.0
726     */
727    public function callback_action_flush_news_sitemap_cache() {
728        delete_transient( 'jetpack_news_sitemap_xml' );
729    }
730
731    /**
732     * Callback for resetting stored sitemap data.
733     *
734     * @access public
735     * @since 5.3.0
736     * @since 6.7.0 Schedules a regeneration.
737     */
738    public function callback_action_purge_data() {
739        $this->callback_action_flush_news_sitemap_cache();
740        $this->librarian->delete_all_stored_sitemap_data();
741        /** This filter is documented in modules/sitemaps/sitemaps.php */
742        $delay = apply_filters( 'jetpack_sitemap_generation_delay', MINUTE_IN_SECONDS * wp_rand( 1, 15 ) ); // Randomly space it out to start within next fifteen minutes.
743        wp_schedule_single_event( time() + $delay, 'jp_sitemap_cron_hook' );
744    }
745
746    /**
747     * Callback to set the sitemap location.
748     *
749     * @access public
750     * @since 4.8.0
751     */
752    public function callback_action_filter_sitemap_location() {
753        update_option(
754            'jetpack_sitemap_location',
755            /**
756             * Additional path for sitemap URIs. Default value is empty.
757             *
758             * This string is any additional path fragment you want included between
759             * the home URL and the sitemap filenames. Exactly how this fragment is
760             * interpreted depends on your permalink settings. For example:
761             *
762             *   Pretty permalinks:
763             *     home_url() . jetpack_sitemap_location . '/sitemap.xml'
764             *
765             *   Plain ("ugly") permalinks:
766             *     home_url() . jetpack_sitemap_location . '/?jetpack-sitemap=sitemap.xml'
767             *
768             *   PATHINFO permalinks:
769             *     home_url() . '/index.php' . jetpack_sitemap_location . '/sitemap.xml'
770             *
771             * where 'sitemap.xml' is the name of a specific sitemap file.
772             * The value of this filter must be a valid path fragment per RFC 3986;
773             * in particular it must either be empty or begin with a '/'.
774             * Also take care that any restrictions on sitemap location imposed by
775             * the sitemap protocol are satisfied.
776             *
777             * The result of this filter is stored in an option, 'jetpack_sitemap_location';
778             * that option is what gets read when the sitemap location is needed.
779             * This way we don't have to wait for init to finish before building sitemaps.
780             *
781             * @link https://tools.ietf.org/html/rfc3986#section-3.3 RFC 3986
782             * @link https://www.sitemaps.org/ The sitemap protocol
783             *
784             * @since 4.8.0
785             */
786            apply_filters(
787                'jetpack_sitemap_location',
788                ''
789            )
790        );
791    }
792} // End Jetpack_Sitemap_Manager class.
793
794new Jetpack_Sitemap_Manager();
795
796/**
797 * Absolute URL of the current blog's sitemap.
798 *
799 * @module sitemaps
800 *
801 * @since  3.9.0
802 * @since  4.8.1 Code uses method found in Jetpack_Sitemap_Finder::construct_sitemap_url in 4.8.0.
803 *                It has been moved here to avoid fatal errors with other plugins that were expecting to find this function.
804 *
805 * @param string $filename Sitemap file name. Defaults to 'sitemap.xml', the initial sitemaps page.
806 *
807 * @return string Sitemap URL.
808 */
809function jetpack_sitemap_uri( $filename = 'sitemap.xml' ) {
810    global $wp_rewrite;
811
812    $location = Jetpack_Options::get_option_and_ensure_autoload( 'jetpack_sitemap_location', '' );
813
814    if ( $wp_rewrite->using_index_permalinks() ) {
815        $sitemap_url = home_url( '/index.php' . $location . '/' . $filename );
816    } elseif ( $wp_rewrite->using_permalinks() ) {
817        $sitemap_url = home_url( $location . '/' . $filename );
818    } else {
819        $sitemap_url = home_url( $location . '/?jetpack-sitemap=' . $filename );
820    }
821
822    /**
823     * Filter sitemap URL relative to home URL.
824     *
825     * @module sitemaps
826     *
827     * @since 3.9.0
828     *
829     * @param string $sitemap_url Sitemap URL.
830     */
831    return apply_filters( 'jetpack_sitemap_location', $sitemap_url );
832}
833
834// Register Jetpack Sitemaps abilities (WordPress Abilities API, WP 6.9+).
835require_once __DIR__ . '/abilities/class-sitemaps-abilities.php';
836\Automattic\Jetpack\Plugin\Abilities\Sitemaps_Abilities::init();