Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
60.44% covered (warning)
60.44%
437 / 723
32.00% covered (danger)
32.00%
8 / 25
CRAP
0.00% covered (danger)
0.00%
0 / 2
Jetpack_Sitemap_Buffer_Empty
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 2
12
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 get_root_element
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
Jetpack_Sitemap_Builder
63.98% covered (warning)
63.98%
437 / 683
34.78% covered (danger)
34.78%
8 / 23
1156.67
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 update_sitemap
65.00% covered (warning)
65.00%
13 / 20
0.00% covered (danger)
0.00%
0 / 1
7.54
 build_next_sitemap_file
89.47% covered (warning)
89.47%
51 / 57
0.00% covered (danger)
0.00%
0 / 1
11.14
 build_next_sitemap_of_type
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
1 / 1
5
 build_next_sitemap_index_of_type
75.00% covered (warning)
75.00%
48 / 64
0.00% covered (danger)
0.00%
0 / 1
10.27
 build_master_sitemap
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
6
 create_master_buffer
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 build_flat_master_buffer
90.48% covered (success)
90.48%
19 / 21
0.00% covered (danger)
0.00%
0 / 1
10.09
 build_nested_master_buffer
69.23% covered (warning)
69.23%
18 / 26
0.00% covered (danger)
0.00%
0 / 1
14.52
 sitemap_count_of
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 stored_sitemap_timestamps
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 append_sitemap_to_master
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 build_one_page_sitemap
91.46% covered (success)
91.46%
75 / 82
0.00% covered (danger)
0.00%
0 / 1
22.30
 build_one_image_sitemap
53.85% covered (warning)
53.85%
21 / 39
0.00% covered (danger)
0.00%
0 / 1
14.29
 build_one_video_sitemap
52.50% covered (warning)
52.50%
21 / 40
0.00% covered (danger)
0.00%
0 / 1
17.68
 build_one_sitemap_index
74.55% covered (warning)
74.55%
41 / 55
0.00% covered (danger)
0.00%
0 / 1
10.34
 sitemap_row_to_index_item
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 empty_sitemap_xml
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 news_sitemap_xml
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
90
 post_to_sitemap_item
64.00% covered (warning)
64.00%
16 / 25
0.00% covered (danger)
0.00%
0 / 1
4.75
 image_post_to_sitemap_item
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
20
 video_post_to_sitemap_item
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
42
 post_to_news_sitemap_item
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
20
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Build the sitemap tree.
4 *
5 * @package automattic/jetpack
6 * @since 4.8.0
7 * @author Automattic
8 */
9
10if ( ! defined( 'ABSPATH' ) ) {
11    exit( 0 );
12}
13
14/* Include sitemap subclasses, if not already, and include proper buffer based on phpxml's availability. */
15require_once __DIR__ . '/sitemap-constants.php';
16require_once __DIR__ . '/sitemap-buffer.php';
17
18if ( ! class_exists( 'DOMDocument' ) ) {
19    require_once __DIR__ . '/sitemap-buffer-fallback.php';
20    require_once __DIR__ . '/sitemap-buffer-image-fallback.php';
21    require_once __DIR__ . '/sitemap-buffer-master-fallback.php';
22    require_once __DIR__ . '/sitemap-buffer-news-fallback.php';
23    require_once __DIR__ . '/sitemap-buffer-page-fallback.php';
24    require_once __DIR__ . '/sitemap-buffer-video-fallback.php';
25} else {
26    require_once __DIR__ . '/sitemap-buffer-image.php';
27    require_once __DIR__ . '/sitemap-buffer-master.php';
28    require_once __DIR__ . '/sitemap-buffer-news.php';
29    require_once __DIR__ . '/sitemap-buffer-page.php';
30    require_once __DIR__ . '/sitemap-buffer-video.php';
31}
32
33require_once __DIR__ . '/sitemap-librarian.php';
34require_once __DIR__ . '/sitemap-finder.php';
35require_once __DIR__ . '/sitemap-state.php';
36
37if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
38    require_once __DIR__ . '/sitemap-logger.php';
39}
40
41/**
42 * Simple class for rendering an empty sitemap with a short TTL
43 */
44class Jetpack_Sitemap_Buffer_Empty extends Jetpack_Sitemap_Buffer {
45    /**
46     * Jetpack_Sitemap_Buffer_Empty constructor.
47     */
48    public function __construct() {
49        parent::__construct( JP_SITEMAP_MAX_ITEMS, JP_SITEMAP_MAX_BYTES, '1970-01-01 00:00:00' );
50
51        $this->doc->appendChild(
52            $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" )
53        );
54
55        $this->doc->appendChild(
56            $this->doc->createComment( 'Jetpack_Sitemap_Buffer_Empty' )
57        );
58
59        $this->doc->appendChild(
60            $this->doc->createProcessingInstruction(
61                'xml-stylesheet',
62                'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'sitemap-index.xsl' ) . '"'
63            )
64        );
65    }
66
67    /**
68     * Returns a DOM element for an empty sitemap.
69     */
70    protected function get_root_element() {
71        if ( ! isset( $this->root ) ) {
72            $this->root = $this->doc->createElement( 'sitemapindex' );
73            $this->root->setAttribute( 'xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9' );
74            $this->doc->appendChild( $this->root );
75            $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) );
76        }
77
78        return $this->root;
79    }
80}
81
82/**
83 * The Jetpack_Sitemap_Builder object handles the construction of
84 * all sitemap files (except the XSL files, which are handled by
85 * Jetpack_Sitemap_Stylist.) Other than the constructor, there are
86 * only two public functions: build_all_sitemaps and news_sitemap_xml.
87 *
88 * @since 4.8.0
89 */
90class Jetpack_Sitemap_Builder { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound,Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
91
92    /**
93     * Returned by the master sitemap builders when the entries do not fit in one
94     * buffer, so the flat listing has to give way to the nested indexes.
95     *
96     * @since 16.2
97     */
98    const MASTER_OVERFLOW = 'overflow';
99
100    /**
101     * Returned by the master sitemap builders when a sitemap file they would link
102     * is missing, so any master built now would reach fewer URLs than the last one.
103     *
104     * @since 16.2
105     */
106    const MASTER_INCOMPLETE = 'incomplete';
107
108    /**
109     * Librarian object for storing and retrieving sitemap data.
110     *
111     * @access private
112     * @since 4.8.0
113     * @var $librarian Jetpack_Sitemap_Librarian
114     */
115    private $librarian;
116
117    /**
118     * Logger object for reporting debug messages.
119     *
120     * @access private
121     * @since 4.8.0
122     * @var $logger Jetpack_Sitemap_Logger
123     */
124    private $logger = false;
125
126    /**
127     * Finder object for dealing with sitemap URIs.
128     *
129     * @access private
130     * @since 4.8.0
131     * @var $finder Jetpack_Sitemap_Finder
132     */
133    private $finder;
134
135    /**
136     * Construct a new Jetpack_Sitemap_Builder object.
137     *
138     * @access public
139     * @since 4.8.0
140     */
141    public function __construct() {
142        $this->librarian = new Jetpack_Sitemap_Librarian();
143        $this->finder    = new Jetpack_Sitemap_Finder();
144
145        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
146            $this->logger = new Jetpack_Sitemap_Logger();
147        }
148
149        update_option(
150            'jetpack_sitemap_post_types',
151            /**
152             * The array of post types to be included in the sitemap.
153             *
154             * Add your custom post type name to the array to have posts of
155             * that type included in the sitemap. The default array includes
156             * 'page' and 'post'.
157             *
158             * The result of this filter is cached in an option, 'jetpack_sitemap_post_types',
159             * so this filter only has to be applied once per generation.
160             *
161             * @since 4.8.0
162             */
163            apply_filters(
164                'jetpack_sitemap_post_types',
165                array( 'post', 'page' )
166            )
167        );
168    }
169
170    /**
171     * Update the sitemap.
172     *
173     * All we do here is call build_next_sitemap_file a bunch of times.
174     *
175     * @since 4.8.0
176     */
177    public function update_sitemap() {
178        if ( $this->logger ) {
179            $this->logger->report( '-- Updating...' );
180            if ( ! class_exists( 'DOMDocument' ) ) {
181                $this->logger->report(
182                    __(
183                        'Jetpack cannot load necessary XML manipulation libraries. Please ask your hosting provider to refer to our server requirements at https://jetpack.com/support/server-requirements/ .',
184                        'jetpack'
185                    ),
186                    true
187                );
188            }
189        }
190
191        /**
192         * Filters whether to suspend cache addition for the entire sitemap generation.
193         *
194         * @since 15.0
195         *
196         * @param bool|null $suspend_addition Whether to suspend cache addition. Defaults to null.
197         * @return bool|null Whether to suspend cache addition.
198         */
199        $suspend_addition = apply_filters( 'jetpack_sitemap_suspend_cache_addition', null );
200
201        // Cache the previous state in case something else changed it.
202        $prev_suspend_addition = wp_suspend_cache_addition();
203
204        wp_suspend_cache_addition( $suspend_addition );
205
206        for ( $i = 1; $i <= JP_SITEMAP_UPDATE_SIZE; $i++ ) {
207            if ( true === $this->build_next_sitemap_file() ) {
208                break; // All finished!
209            }
210        }
211
212        // Restore previous state.
213        wp_suspend_cache_addition( $prev_suspend_addition );
214
215        if ( $this->logger ) {
216            $this->logger->report( '-- ...done for now.' );
217            $this->logger->time();
218        }
219    }
220
221    /**
222     * Generate the next sitemap file.
223     *
224     * Reads the most recent state of the sitemap generation phase,
225     * constructs the next file, and updates the state.
226     *
227     * @since 4.8.0
228     *
229     * @return bool True when finished.
230     */
231    private function build_next_sitemap_file() {
232        $finished = false; // Initialize finished flag.
233
234        // Get the most recent state, and lock the state.
235        $state = Jetpack_Sitemap_State::check_out();
236
237        // Do nothing if the state was locked.
238        if ( false === $state ) {
239            return false;
240        }
241
242        // Otherwise, branch on the sitemap-type key of $state.
243        switch ( $state['sitemap-type'] ) {
244            case JP_PAGE_SITEMAP_TYPE:
245                $this->build_next_sitemap_of_type(
246                    JP_PAGE_SITEMAP_TYPE,
247                    array( $this, 'build_one_page_sitemap' ),
248                    $state
249                );
250                break;
251
252            case JP_PAGE_SITEMAP_INDEX_TYPE:
253                $this->build_next_sitemap_index_of_type(
254                    JP_PAGE_SITEMAP_INDEX_TYPE,
255                    JP_IMAGE_SITEMAP_TYPE,
256                    $state
257                );
258                break;
259
260            case JP_IMAGE_SITEMAP_TYPE:
261                $this->build_next_sitemap_of_type(
262                    JP_IMAGE_SITEMAP_TYPE,
263                    array( $this, 'build_one_image_sitemap' ),
264                    $state
265                );
266                break;
267
268            case JP_IMAGE_SITEMAP_INDEX_TYPE:
269                $this->build_next_sitemap_index_of_type(
270                    JP_IMAGE_SITEMAP_INDEX_TYPE,
271                    JP_VIDEO_SITEMAP_TYPE,
272                    $state
273                );
274                break;
275
276            case JP_VIDEO_SITEMAP_TYPE:
277                $this->build_next_sitemap_of_type(
278                    JP_VIDEO_SITEMAP_TYPE,
279                    array( $this, 'build_one_video_sitemap' ),
280                    $state
281                );
282                break;
283
284            case JP_VIDEO_SITEMAP_INDEX_TYPE:
285                $this->build_next_sitemap_index_of_type(
286                    JP_VIDEO_SITEMAP_INDEX_TYPE,
287                    JP_MASTER_SITEMAP_TYPE,
288                    $state
289                );
290                break;
291
292            case JP_MASTER_SITEMAP_TYPE:
293                $this->build_master_sitemap( $state['max'] );
294
295                // Reset the state and quit.
296                Jetpack_Sitemap_State::reset(
297                    JP_PAGE_SITEMAP_TYPE
298                );
299
300                if ( $this->logger ) {
301                    $this->logger->report( '-- Finished.' );
302                    $this->logger->time();
303                }
304                $finished = true;
305
306                break;
307
308            default:
309                Jetpack_Sitemap_State::reset(
310                    JP_PAGE_SITEMAP_TYPE
311                );
312                $finished = true;
313
314                break;
315        } // End switch.
316
317        // Unlock the state.
318        Jetpack_Sitemap_State::unlock();
319
320        return $finished;
321    }
322
323    /**
324     * Build the next sitemap of a given type and update the sitemap state.
325     *
326     * @since 4.8.0
327     *
328     * @param string   $sitemap_type The type of the sitemap being generated.
329     * @param callback $build_one    A callback which builds a single sitemap file.
330     * @param array    $state        A sitemap state.
331     */
332    private function build_next_sitemap_of_type( $sitemap_type, $build_one, $state ) {
333        $index_type = jp_sitemap_index_type_of( $sitemap_type );
334
335        // Try to build a sitemap.
336        $result = call_user_func_array(
337            $build_one,
338            array(
339                $state['number'] + 1,
340                $state['last-added'],
341            )
342        );
343
344        if ( false === $result ) {
345            // If no sitemap was generated, advance to the next type.
346            Jetpack_Sitemap_State::check_in(
347                array(
348                    'sitemap-type'  => $index_type,
349                    'last-added'    => 0,
350                    'number'        => 0,
351                    'last-modified' => '1970-01-01 00:00:00',
352                )
353            );
354
355            if ( $this->logger ) {
356                $this->logger->report( "-- Cleaning Up $sitemap_type" );
357            }
358
359            // Clean up old files.
360            $this->librarian->delete_numbered_sitemap_rows_after(
361                $state['number'],
362                $sitemap_type
363            );
364
365            return;
366        }
367
368        // Otherwise, update the state.
369        Jetpack_Sitemap_State::check_in(
370            array(
371                'sitemap-type'  => $state['sitemap-type'],
372                'last-added'    => $result['last_id'],
373                'number'        => $state['number'] + 1,
374                'last-modified' => $result['last_modified'],
375            )
376        );
377
378        if ( true === $result['any_left'] ) {
379            // If there's more work to be done with this type, return.
380            return;
381        }
382
383        // Otherwise, advance state to the next sitemap type.
384        Jetpack_Sitemap_State::check_in(
385            array(
386                'sitemap-type'  => $index_type,
387                'last-added'    => 0,
388                'number'        => 0,
389                'last-modified' => '1970-01-01 00:00:00',
390            )
391        );
392
393        if ( $this->logger ) {
394            $this->logger->report( "-- Cleaning Up $sitemap_type" );
395        }
396
397        // Clean up old files.
398        $this->librarian->delete_numbered_sitemap_rows_after(
399            $state['number'] + 1,
400            $sitemap_type
401        );
402    }
403
404    /**
405     * Build the next sitemap index of a given type and update the state.
406     *
407     * @since 4.8.0
408     *
409     * @param string $index_type The type of index being generated.
410     * @param string $next_type  The next type to generate after this one.
411     * @param array  $state      A sitemap state.
412     */
413    private function build_next_sitemap_index_of_type( $index_type, $next_type, $state ) {
414        $sitemap_type = jp_sitemap_child_type_of( $index_type );
415
416        $sitemap_type_exists = isset( $state['max'][ $sitemap_type ] ) && is_array( $state['max'][ $sitemap_type ] );
417
418        // If only 0 or 1 sitemaps were built, advance to the next type and return.
419        if ( $sitemap_type_exists && 1 >= $state['max'][ $sitemap_type ]['number'] ) {
420            Jetpack_Sitemap_State::check_in(
421                array(
422                    'sitemap-type'  => $next_type,
423                    'last-added'    => 0,
424                    'number'        => 0,
425                    'last-modified' => '1970-01-01 00:00:00',
426                )
427            );
428
429            if ( $this->logger ) {
430                $this->logger->report( "-- Cleaning Up $index_type" );
431            }
432
433            // There are no indices of this type.
434            $this->librarian->delete_numbered_sitemap_rows_after(
435                0,
436                $index_type
437            );
438
439            return;
440        }
441
442        // Otherwise, try to build a sitemap index.
443        $result = $this->build_one_sitemap_index(
444            $state['number'] + 1,
445            $state['last-added'],
446            $state['last-modified'],
447            $index_type
448        );
449
450        // If no index was built, advance to the next type and return.
451        if ( false === $result ) {
452            Jetpack_Sitemap_State::check_in(
453                array(
454                    'sitemap-type'  => $next_type,
455                    'last-added'    => 0,
456                    'number'        => 0,
457                    'last-modified' => '1970-01-01 00:00:00',
458                )
459            );
460
461            if ( $this->logger ) {
462                $this->logger->report( "-- Cleaning Up $index_type" );
463            }
464
465            // Clean up old files.
466            $this->librarian->delete_numbered_sitemap_rows_after(
467                $state['number'],
468                $index_type
469            );
470
471            return;
472        }
473
474        // Otherwise, check in the state.
475        Jetpack_Sitemap_State::check_in(
476            array(
477                'sitemap-type'  => $index_type,
478                'last-added'    => $result['last_id'],
479                'number'        => $state['number'] + 1,
480                'last-modified' => $result['last_modified'],
481            )
482        );
483
484        // If there are still sitemaps left to index, return.
485        if ( true === $result['any_left'] ) {
486            return;
487        }
488
489        // Otherwise, advance to the next type.
490        Jetpack_Sitemap_State::check_in(
491            array(
492                'sitemap-type'  => $next_type,
493                'last-added'    => 0,
494                'number'        => 0,
495                'last-modified' => '1970-01-01 00:00:00',
496            )
497        );
498
499        if ( $this->logger ) {
500            $this->logger->report( "-- Cleaning Up $index_type" );
501        }
502
503        // We're done generating indices of this type.
504        $this->librarian->delete_numbered_sitemap_rows_after(
505            $state['number'] + 1,
506            $index_type
507        );
508    }
509
510    /**
511     * Builds the master sitemap index.
512     *
513     * A sitemap index file may not list other sitemap index files, so with the
514     * `jetpack_sitemap_flat_master_index` filter on, the master lists every
515     * individual sitemap file directly whenever they all fit in one buffer. They
516     * no longer fit somewhere north of a million URLs, and only then does it fall
517     * back to linking the per-type `*-sitemap-index-N.xml` files. With the filter
518     * off, which is still the default, it always links them.
519     *
520     * Either way the buffer is only stored once every sitemap this generation
521     * cycle recorded is accounted for. If one is missing, the master the previous
522     * cycle stored is left in place: it reaches more URLs than anything that
523     * could be built right now.
524     *
525     * @link https://www.sitemaps.org/protocol.html#index
526     *
527     * @param array $max Array of sitemap types with max index and datetime.
528     *
529     * @since 4.8.0
530     */
531    private function build_master_sitemap( $max ) {
532        if ( $this->logger ) {
533            $this->logger->report( '-- Building Master Sitemap.' );
534        }
535
536        $sitemap_types = array(
537            JP_PAGE_SITEMAP_TYPE,
538            JP_IMAGE_SITEMAP_TYPE,
539            JP_VIDEO_SITEMAP_TYPE,
540        );
541
542        $buffer = null;
543
544        /**
545         * Whether the master sitemap lists each individual sitemap file directly.
546         *
547         * A sitemap index file may not list other sitemap index files, and Google
548         * Search Console reports the nested layout as "Nested indexing". Listing
549         * the files directly is what fixes that.
550         *
551         * Off by default so the flat layout can be rolled out a site at a time.
552         * The default is expected to flip once it has been verified in production,
553         * at which point this filter goes away.
554         *
555         * @module sitemaps
556         *
557         * @since 16.2
558         *
559         * @param bool $flat_master_index Whether to list sitemap files directly. Default false.
560         */
561        if ( apply_filters( 'jetpack_sitemap_flat_master_index', false ) ) {
562            $buffer = $this->build_flat_master_buffer( $sitemap_types, $max );
563        }
564
565        if ( null === $buffer || self::MASTER_OVERFLOW === $buffer ) {
566            /*
567             * Either the flat layout is off, or the files did not fit in one
568             * buffer. Nesting is invalid and Google flags it, but a complete
569             * invalid tree beats a valid one that drops URLs, and a sitemap
570             * index cannot be paginated to make the flat layout scale further.
571             */
572            $buffer = $this->build_nested_master_buffer( $sitemap_types, $max );
573        }
574
575        if ( ! is_object( $buffer ) ) {
576            return;
577        }
578
579        $this->librarian->store_sitemap_data(
580            0,
581            JP_MASTER_SITEMAP_TYPE,
582            $buffer->contents(),
583            ''
584        );
585    }
586
587    /**
588     * Create the buffer a master sitemap is assembled in.
589     *
590     * Extracted as a protected seam so a test can hand back a buffer small
591     * enough to overflow without generating a million URLs.
592     *
593     * @access protected
594     * @since 16.2
595     *
596     * @return Jetpack_Sitemap_Buffer|Jetpack_Sitemap_Buffer_XMLWriter|false The buffer, or false if one cannot be created.
597     */
598    protected function create_master_buffer() {
599        return Jetpack_Sitemap_Buffer_Factory::create(
600            'master',
601            JP_SITEMAP_MAX_ITEMS,
602            JP_SITEMAP_MAX_BYTES
603        );
604    }
605
606    /**
607     * Build a master sitemap buffer listing every individual sitemap file.
608     *
609     * The buffer's item and byte limits can both stop this short, and how soon
610     * the byte limit bites depends on how long this site's URLs are, so the
611     * partial buffer is discarded and MASTER_OVERFLOW returned rather than
612     * storing a master that omits sitemaps. Every type is still checked over
613     * once the buffer overflows, so a missing file is reported as
614     * MASTER_INCOMPLETE instead of sending the caller off to build a nested
615     * master out of the same broken state.
616     *
617     * @access private
618     * @since 16.2
619     *
620     * @param array $sitemap_types The sitemap types to list, in order.
621     * @param array $max           Array of sitemap types with max index and datetime.
622     *
623     * @return Jetpack_Sitemap_Buffer|Jetpack_Sitemap_Buffer_XMLWriter|string The buffer, or MASTER_OVERFLOW / MASTER_INCOMPLETE.
624     */
625    private function build_flat_master_buffer( $sitemap_types, $max ) {
626        $buffer = $this->create_master_buffer();
627
628        if ( ! $buffer ) {
629            return self::MASTER_INCOMPLETE;
630        }
631
632        $overflowed = false;
633
634        foreach ( $sitemap_types as $sitemap_type ) {
635            $expected = $this->sitemap_count_of( $max, $sitemap_type );
636
637            if ( $expected < 1 ) {
638                continue;
639            }
640
641            $timestamps = $this->stored_sitemap_timestamps( $sitemap_type, $expected );
642
643            if ( false === $timestamps ) {
644                return self::MASTER_INCOMPLETE;
645            }
646
647            if ( $overflowed ) {
648                continue;
649            }
650
651            for ( $number = 1; $number <= $expected; $number++ ) {
652                $filename = jp_sitemap_filename( $sitemap_type, $number );
653
654                if ( ! $this->append_sitemap_to_master( $buffer, $filename, $timestamps[ $filename ] ) ) {
655                    if ( $this->logger ) {
656                        $this->logger->report( '-- Master Sitemap is full; falling back to nested indexes.' );
657                    }
658
659                    $overflowed = true;
660                    break;
661                }
662            }
663        }
664
665        return $overflowed ? self::MASTER_OVERFLOW : $buffer;
666    }
667
668    /**
669     * Build a master sitemap buffer linking one file per sitemap type: the single
670     * sitemap when there is only one, otherwise that type's newest index file.
671     *
672     * Only used when the individual files do not all fit. The whole index chain
673     * has to be intact, because the newest index reaches the older ones only by
674     * linking back through them, so every index of a type is checked and not just
675     * the one the master names.
676     *
677     * @access private
678     * @since 16.2
679     *
680     * @param array $sitemap_types The sitemap types to list, in order.
681     * @param array $max           Array of sitemap types with max index and datetime.
682     *
683     * @return Jetpack_Sitemap_Buffer|Jetpack_Sitemap_Buffer_XMLWriter|string The buffer, or MASTER_INCOMPLETE.
684     */
685    private function build_nested_master_buffer( $sitemap_types, $max ) {
686        $buffer = $this->create_master_buffer();
687
688        if ( ! $buffer ) {
689            return self::MASTER_INCOMPLETE;
690        }
691
692        foreach ( $sitemap_types as $sitemap_type ) {
693            $expected = $this->sitemap_count_of( $max, $sitemap_type );
694
695            if ( $expected < 1 ) {
696                continue;
697            }
698
699            // The files reached through the index still have to be there.
700            $timestamps = $this->stored_sitemap_timestamps( $sitemap_type, $expected );
701
702            if ( false === $timestamps ) {
703                return self::MASTER_INCOMPLETE;
704            }
705
706            $linked_type  = $sitemap_type;
707            $linked_count = $expected;
708
709            if ( 1 !== $expected ) {
710                // Only a type with a single sitemap has no index to link.
711                $linked_type  = jp_sitemap_index_type_of( $sitemap_type );
712                $linked_count = $this->sitemap_count_of( $max, $linked_type );
713                $timestamps   = $this->stored_sitemap_timestamps( $linked_type, $linked_count );
714
715                if ( $linked_count < 1 || false === $timestamps ) {
716                    if ( $this->logger ) {
717                        $this->logger->report( "-- No usable index for $sitemap_type; keeping the previous Master Sitemap." );
718                    }
719
720                    return self::MASTER_INCOMPLETE;
721                }
722            }
723
724            $linked_name = jp_sitemap_filename( $linked_type, $linked_count );
725
726            if ( ! $this->append_sitemap_to_master( $buffer, $linked_name, $timestamps[ $linked_name ] ) ) {
727                if ( $this->logger ) {
728                    $this->logger->report( "-- No room for $linked_name; keeping the previous Master Sitemap." );
729                }
730
731                return self::MASTER_INCOMPLETE;
732            }
733        }
734
735        return $buffer;
736    }
737
738    /**
739     * The number of sitemap files of a type the current generation cycle recorded.
740     *
741     * @access private
742     * @since 16.2
743     *
744     * @param array  $max  Array of sitemap types with max index and datetime.
745     * @param string $type A sitemap or sitemap index type.
746     *
747     * @return int The count, or 0 if the type produced nothing.
748     */
749    private function sitemap_count_of( $max, $type ) {
750        return isset( $max[ $type ]['number'] ) ? (int) $max[ $type ]['number'] : 0;
751    }
752
753    /**
754     * Look up the timestamps of files 1..$count of a sitemap type, by filename.
755     *
756     * Filenames rather than row order, because a row rewritten by an interrupted
757     * cleanup no longer sorts where its number says it should.
758     *
759     * @access private
760     * @since 16.2
761     *
762     * @param string $type  A sitemap or sitemap index type.
763     * @param int    $count How many files of that type to expect.
764     *
765     * @return array|false Map of filename to timestamp, or false if any is missing.
766     */
767    private function stored_sitemap_timestamps( $type, $count ) {
768        $names = array();
769
770        for ( $number = 1; $number <= $count; $number++ ) {
771            $names[] = jp_sitemap_filename( $type, $number );
772        }
773
774        $timestamps = $this->librarian->query_sitemap_timestamps( $type, $names );
775
776        foreach ( $names as $name ) {
777            if ( ! isset( $timestamps[ $name ] ) ) {
778                if ( $this->logger ) {
779                    $this->logger->report( "-- $name is missing; keeping the previous Master Sitemap." );
780                }
781
782                return false;
783            }
784        }
785
786        return $timestamps;
787    }
788
789    /**
790     * Append one <sitemap> entry to a master sitemap buffer.
791     *
792     * @access private
793     * @since 16.2
794     *
795     * @param Jetpack_Sitemap_Buffer|Jetpack_Sitemap_Buffer_XMLWriter $buffer   The master sitemap buffer.
796     * @param string                                                  $filename The sitemap filename to link.
797     * @param string                                                  $lastmod  Its timestamp, in 'YYYY-MM-DD hh:mm:ss' format.
798     *
799     * @return bool Whether the entry fit.
800     */
801    private function append_sitemap_to_master( $buffer, $filename, $lastmod ) {
802        return true === $buffer->append(
803            array(
804                'sitemap' => array(
805                    'loc'     => $this->finder->construct_sitemap_url( $filename ),
806                    'lastmod' => jp_sitemap_datetime( $lastmod ),
807                ),
808            )
809        );
810    }
811
812    /**
813     * Build and store a single page sitemap. Returns false if no sitemap is built.
814     *
815     * Side effect: Create/update a sitemap row.
816     *
817     * @access private
818     * @since 4.8.0
819     *
820     * @param int $number The number of the current sitemap.
821     * @param int $from_id The greatest lower bound of the IDs of the posts to be included.
822     *
823     * @return bool|array @args {
824     *   @type int    $last_id       The ID of the last item to be successfully added to the buffer.
825     *   @type bool   $any_left      'true' if there are items which haven't been saved to a sitemap, 'false' otherwise.
826     *   @type string $last_modified The most recent timestamp to appear on the sitemap.
827     * }
828     */
829    public function build_one_page_sitemap( $number, $from_id ) {
830        $last_post_id   = $from_id;
831        $any_posts_left = true;
832
833        if ( $this->logger ) {
834            $debug_name = jp_sitemap_filename( JP_PAGE_SITEMAP_TYPE, $number );
835            $this->logger->report( "-- Building $debug_name" );
836        }
837
838        $buffer = Jetpack_Sitemap_Buffer_Factory::create(
839            'page',
840            JP_SITEMAP_MAX_ITEMS,
841            JP_SITEMAP_MAX_BYTES
842        );
843
844        if ( ! $buffer ) {
845            return false;
846        }
847
848        // Add entry for the main page (only if we're at the first one) and it isn't already going to be included as a page.
849        if ( 1 === $number && 'page' !== get_option( 'show_on_front' ) ) {
850            $item_array = array(
851                'url' => array(
852                    'loc' => home_url( '/' ),
853                ),
854            );
855
856            /**
857             * Filter associative array with data to build <url> node
858             * and its descendants for site home.
859             *
860             * @module sitemaps
861             *
862             * @since 3.9.0
863             *
864             * @param array $blog_home Data to build parent and children nodes for site home.
865             */
866            $item_array = apply_filters( 'jetpack_sitemap_url_home', $item_array );
867
868            $buffer->append( $item_array );
869        }
870
871        // Add as many items to the buffer as possible.
872        while ( $last_post_id >= 0 && false === $buffer->is_full() ) {
873            $posts = $this->librarian->query_posts_after_id(
874                $last_post_id,
875                JP_SITEMAP_BATCH_SIZE
876            );
877
878            if ( null == $posts ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- WPCS: loose comparison ok.
879                $any_posts_left = false;
880                break;
881            }
882
883            foreach ( $posts as $post ) {
884                $current_item = $this->post_to_sitemap_item( $post );
885
886                if ( true === $buffer->append( $current_item['xml'] ) ) {
887                    $last_post_id = $post->ID;
888                    $buffer->view_time( $current_item['last_modified'] );
889                } else {
890                    break;
891                }
892            }
893        }
894
895        // Handle other page sitemap URLs.
896        if ( ! $any_posts_left || $last_post_id < 0 ) {
897            // Negative IDs are used to track URL indexes.
898            $last_post_id   = min( 0, $last_post_id );
899            $any_posts_left = true; // Reinitialize.
900
901            /**
902             * Filter other page sitemap URLs.
903             *
904             * @module sitemaps
905             *
906             * @since 6.1.0
907             *
908             * @param array $urls An array of other URLs.
909             */
910            $other_urls = apply_filters( 'jetpack_page_sitemap_other_urls', array() );
911
912            if ( $other_urls ) { // Start with index [1].
913                $other_urls = array_values( $other_urls );
914                array_unshift( $other_urls, $other_urls[0] );
915                unset( $other_urls[0] );
916            }
917
918            // Add as many items to the buffer as possible.
919            while ( false === $buffer->is_full() ) {
920                $last_post_id_index       = abs( $last_post_id );
921                $start_from_post_id_index = $last_post_id_index ? $last_post_id_index + 1 : 0;
922                $urls                     = array_slice(
923                    $other_urls,
924                    $start_from_post_id_index,
925                    JP_SITEMAP_BATCH_SIZE,
926                    true
927                );
928
929                if ( ! $urls ) {
930                    $any_posts_left = false;
931                    break;
932                }
933
934                foreach ( $urls as $index => $url ) {
935                    if ( ! is_array( $url ) ) {
936                        $url = array( 'loc' => $url );
937                    }
938                    $item = array( 'xml' => compact( 'url' ) );
939
940                    if ( true === $buffer->append( $item['xml'] ) ) {
941                        $last_post_id = -$index;
942                        if ( isset( $url['lastmod'] ) ) {
943                            $buffer->view_time( jp_sitemap_datetime( $url['lastmod'] ) );
944                        }
945                    } else {
946                        break;
947                    }
948                }
949            }
950        }
951
952        // If no items were added, return false.
953        if ( true === $buffer->is_empty() ) {
954            return false;
955        }
956
957        /**
958         * Filter sitemap before rendering it as XML.
959         *
960         * @module sitemaps
961         *
962         * @since 3.9.0
963         * @since 5.3.0 returns an element of DOMDocument type instead of SimpleXMLElement
964         *
965         * @param DOMDocument      $doc Data tree for sitemap.
966         * @param string           $last_modified Date of last modification.
967         */
968        if ( has_filter( 'jetpack_print_sitemap' ) ) {
969            apply_filters(
970                'jetpack_print_sitemap',
971                $buffer->get_document(),
972                $buffer->last_modified()
973            );
974        }
975
976        // Store the buffer as the content of a sitemap row.
977        $this->librarian->store_sitemap_data(
978            $number,
979            JP_PAGE_SITEMAP_TYPE,
980            $buffer->contents(),
981            $buffer->last_modified()
982        );
983
984        /*
985         * Now report back with the ID of the last post ID to be
986         * successfully added and whether there are any posts left.
987         */
988        return array(
989            'last_id'       => $last_post_id,
990            'any_left'      => $any_posts_left,
991            'last_modified' => $buffer->last_modified(),
992        );
993    }
994
995    /**
996     * Build and store a single image sitemap. Returns false if no sitemap is built.
997     *
998     * Side effect: Create/update an image sitemap row.
999     *
1000     * @access private
1001     * @since 4.8.0
1002     *
1003     * @param int $number The number of the current sitemap.
1004     * @param int $from_id The greatest lower bound of the IDs of the posts to be included.
1005     *
1006     * @return bool|array @args {
1007     *   @type int    $last_id       The ID of the last item to be successfully added to the buffer.
1008     *   @type bool   $any_left      'true' if there are items which haven't been saved to a sitemap, 'false' otherwise.
1009     *   @type string $last_modified The most recent timestamp to appear on the sitemap.
1010     * }
1011     */
1012    public function build_one_image_sitemap( $number, $from_id ) {
1013        $last_post_id   = $from_id;
1014        $any_posts_left = true;
1015
1016        if ( $this->logger ) {
1017            $debug_name = jp_sitemap_filename( JP_IMAGE_SITEMAP_TYPE, $number );
1018            $this->logger->report( "-- Building $debug_name" );
1019        }
1020
1021        $buffer = Jetpack_Sitemap_Buffer_Factory::create(
1022            'image',
1023            JP_SITEMAP_MAX_ITEMS,
1024            JP_SITEMAP_MAX_BYTES
1025        );
1026
1027        if ( ! $buffer ) {
1028            return false;
1029        }
1030
1031        // Add as many items to the buffer as possible.
1032        while ( false === $buffer->is_full() ) {
1033            $posts = $this->librarian->query_images_after_id(
1034                $last_post_id,
1035                JP_SITEMAP_BATCH_SIZE
1036            );
1037
1038            if ( null == $posts ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- WPCS: loose comparison ok.
1039                $any_posts_left = false;
1040                break;
1041            }
1042
1043            foreach ( $posts as $post ) {
1044                $current_item = $this->image_post_to_sitemap_item( $post );
1045
1046                if ( true === $buffer->append( $current_item['xml'] ) ) {
1047                    $last_post_id = $post->ID;
1048                    $buffer->view_time( $current_item['last_modified'] );
1049                } else {
1050                    break;
1051                }
1052            }
1053        }
1054
1055        // If no items were added, return false.
1056        if ( true === $buffer->is_empty() ) {
1057            return false;
1058        }
1059
1060        // Store the buffer as the content of a jp_sitemap post.
1061        $this->librarian->store_sitemap_data(
1062            $number,
1063            JP_IMAGE_SITEMAP_TYPE,
1064            $buffer->contents(),
1065            $buffer->last_modified()
1066        );
1067
1068        /*
1069         * Now report back with the ID of the last post to be
1070         * successfully added and whether there are any posts left.
1071         */
1072        return array(
1073            'last_id'       => $last_post_id,
1074            'any_left'      => $any_posts_left,
1075            'last_modified' => $buffer->last_modified(),
1076        );
1077    }
1078
1079    /**
1080     * Build and store a single video sitemap. Returns false if no sitemap is built.
1081     *
1082     * Side effect: Create/update an video sitemap row.
1083     *
1084     * @access private
1085     * @since 4.8.0
1086     *
1087     * @param int $number The number of the current sitemap.
1088     * @param int $from_id The greatest lower bound of the IDs of the posts to be included.
1089     *
1090     * @return bool|array @args {
1091     *   @type int    $last_id       The ID of the last item to be successfully added to the buffer.
1092     *   @type bool   $any_left      'true' if there are items which haven't been saved to a sitemap, 'false' otherwise.
1093     *   @type string $last_modified The most recent timestamp to appear on the sitemap.
1094     * }
1095     */
1096    public function build_one_video_sitemap( $number, $from_id ) {
1097        $last_post_id   = $from_id;
1098        $any_posts_left = true;
1099
1100        if ( $this->logger ) {
1101            $debug_name = jp_sitemap_filename( JP_VIDEO_SITEMAP_TYPE, $number );
1102            $this->logger->report( "-- Building $debug_name" );
1103        }
1104
1105        $buffer = Jetpack_Sitemap_Buffer_Factory::create(
1106            'video',
1107            JP_SITEMAP_MAX_ITEMS,
1108            JP_SITEMAP_MAX_BYTES
1109        );
1110
1111        if ( ! $buffer ) {
1112            return false;
1113        }
1114
1115        // Add as many items to the buffer as possible.
1116        while ( false === $buffer->is_full() ) {
1117            $posts = $this->librarian->query_videos_after_id(
1118                $last_post_id,
1119                JP_SITEMAP_BATCH_SIZE
1120            );
1121
1122            if ( null == $posts ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- WPCS: loose comparison ok.
1123                $any_posts_left = false;
1124                break;
1125            }
1126
1127            foreach ( $posts as $post ) {
1128                $current_item = $this->video_post_to_sitemap_item( $post );
1129
1130                if ( true === $buffer->append( $current_item['xml'] ) ) {
1131                    $last_post_id = $post->ID;
1132                    $buffer->view_time( $current_item['last_modified'] );
1133                } else {
1134                    break;
1135                }
1136            }
1137        }
1138
1139        // If no items were added, return false.
1140        if ( true === $buffer->is_empty() ) {
1141            return false;
1142        }
1143
1144        if ( false === $buffer->is_empty() ) {
1145            $this->librarian->store_sitemap_data(
1146                $number,
1147                JP_VIDEO_SITEMAP_TYPE,
1148                $buffer->contents(),
1149                $buffer->last_modified()
1150            );
1151        }
1152
1153        /*
1154         * Now report back with the ID of the last post to be
1155         * successfully added and whether there are any posts left.
1156         */
1157        return array(
1158            'last_id'       => $last_post_id,
1159            'any_left'      => $any_posts_left,
1160            'last_modified' => $buffer->last_modified(),
1161        );
1162    }
1163
1164    /**
1165     * Build and store a single page sitemap index. Return false if no index is built.
1166     *
1167     * Side effect: Create/update a sitemap index row.
1168     *
1169     * @access private
1170     * @since 4.8.0
1171     *
1172     * @param int    $number     The number of the current sitemap index.
1173     * @param int    $from_id    The greatest lower bound of the IDs of the sitemaps to be included.
1174     * @param string $datetime   Datetime of previous sitemap in 'YYYY-MM-DD hh:mm:ss' format.
1175     * @param string $index_type Sitemap index type.
1176     *
1177     * @return bool|array @args {
1178     *   @type int    $last_id       The ID of the last item to be successfully added to the buffer.
1179     *   @type bool   $any_left      'true' if there are items which haven't been saved to a sitemap, 'false' otherwise.
1180     *   @type string $last_modified The most recent timestamp to appear on the sitemap.
1181     * }
1182     */
1183    private function build_one_sitemap_index( $number, $from_id, $datetime, $index_type ) {
1184        $last_sitemap_id   = $from_id;
1185        $any_sitemaps_left = true;
1186
1187        // Check the datetime format.
1188        $datetime = jp_sitemap_datetime( $datetime );
1189
1190        $sitemap_type = jp_sitemap_child_type_of( $index_type );
1191
1192        if ( $this->logger ) {
1193            $index_debug_name = jp_sitemap_filename( $index_type, $number );
1194            $this->logger->report( "-- Building $index_debug_name" );
1195        }
1196
1197        $buffer = Jetpack_Sitemap_Buffer_Factory::create(
1198            'master',
1199            JP_SITEMAP_MAX_ITEMS,
1200            JP_SITEMAP_MAX_BYTES,
1201            $datetime
1202        );
1203        if ( ! $buffer ) {
1204            return false;
1205        }
1206
1207        // Add pointer to the previous sitemap index (unless we're at the first one).
1208        if ( 1 !== $number ) {
1209            $i              = $number - 1;
1210            $prev_index_url = $this->finder->construct_sitemap_url(
1211                jp_sitemap_filename( $index_type, $i )
1212            );
1213
1214            $item_array = array(
1215                'sitemap' => array(
1216                    'loc'     => $prev_index_url,
1217                    'lastmod' => $datetime,
1218                ),
1219            );
1220
1221            $buffer->append( $item_array );
1222        }
1223
1224        // Add as many items to the buffer as possible.
1225        while ( false === $buffer->is_full() ) {
1226            // Retrieve a batch of posts (in order).
1227            $posts = $this->librarian->query_sitemaps_after_id(
1228                $sitemap_type,
1229                $last_sitemap_id,
1230                JP_SITEMAP_BATCH_SIZE
1231            );
1232
1233            // If there were no posts to get, make a note.
1234            if ( null == $posts ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- WPCS: loose comparison ok.
1235                $any_sitemaps_left = false;
1236                break;
1237            }
1238
1239            // Otherwise, loop through each post in the batch.
1240            foreach ( $posts as $post ) {
1241                // Generate the sitemap XML for the post.
1242                $current_item = $this->sitemap_row_to_index_item( (array) $post );
1243
1244                // Try adding this item to the buffer.
1245                if ( true === $buffer->append( $current_item['xml'] ) ) {
1246                    $last_sitemap_id = $post['ID'];
1247                    $buffer->view_time( $current_item['last_modified'] );
1248                } else {
1249                    // Otherwise stop looping through posts.
1250                    break;
1251                }
1252            }
1253        }
1254
1255        // If no items were added, return false.
1256        if ( true === $buffer->is_empty() ) {
1257            return false;
1258        }
1259
1260        $this->librarian->store_sitemap_data(
1261            $number,
1262            $index_type,
1263            $buffer->contents(),
1264            $buffer->last_modified()
1265        );
1266
1267        /*
1268         * Now report back with the ID of the last sitemap post ID to
1269         * be successfully added, whether there are any sitemap posts
1270         * left, and the most recent modification time seen.
1271         */
1272        return array(
1273            'last_id'       => $last_sitemap_id,
1274            'any_left'      => $any_sitemaps_left,
1275            'last_modified' => $buffer->last_modified(),
1276        );
1277    }
1278
1279    /**
1280     * Construct the sitemap index url entry for a sitemap row.
1281     *
1282     * @link https://www.sitemaps.org/protocol.html#sitemapIndex_sitemap
1283     *
1284     * @access private
1285     * @since 4.8.0
1286     *
1287     * @param array $row The sitemap data to be processed.
1288     *
1289     * @return string An XML fragment representing the post URL.
1290     */
1291    private function sitemap_row_to_index_item( $row ) {
1292        $url = $this->finder->construct_sitemap_url( $row['post_title'] );
1293
1294        $item_array = array(
1295            'sitemap' => array(
1296                'loc'     => $url,
1297                'lastmod' => jp_sitemap_datetime( $row['post_date'] ),
1298            ),
1299        );
1300
1301        return array(
1302            'xml'           => $item_array,
1303            'last_modified' => $row['post_date'],
1304        );
1305    }
1306
1307    /**
1308     * This is served instead of a 404 when the master sitemap is requested
1309     * but not yet generated.
1310     *
1311     * @access public
1312     * @since 6.7.0
1313     *
1314     * @return string The empty sitemap xml.
1315     */
1316    public function empty_sitemap_xml() {
1317        $empty_sitemap = new Jetpack_Sitemap_Buffer_Empty();
1318        return $empty_sitemap->contents();
1319    }
1320
1321    /**
1322     * Build and return the news sitemap xml. Note that the result of this
1323     * function is cached in the transient 'jetpack_news_sitemap_xml'.
1324     *
1325     * @access public
1326     * @since 4.8.0
1327     *
1328     * @return string The news sitemap xml.
1329     */
1330    public function news_sitemap_xml() {
1331        $buffer = Jetpack_Sitemap_Buffer_Factory::create(
1332            'news',
1333            JP_SITEMAP_MAX_ITEMS,
1334            JP_SITEMAP_MAX_BYTES
1335        );
1336
1337        if ( ! $buffer ) {
1338            return '';
1339        }
1340
1341        $the_stored_news_sitemap = get_transient( 'jetpack_news_sitemap_xml' );
1342
1343        if ( false === $the_stored_news_sitemap ) {
1344
1345            if ( $this->logger ) {
1346                $this->logger->report( 'Beginning news sitemap generation.' );
1347            }
1348
1349            /**
1350             * Filter limit of entries to include in news sitemap.
1351             *
1352             * @module sitemaps
1353             *
1354             * @since 3.9.0
1355             *
1356             * @param int $count Number of entries to include in news sitemap.
1357             */
1358            $item_limit = apply_filters(
1359                'jetpack_sitemap_news_sitemap_count',
1360                JP_NEWS_SITEMAP_MAX_ITEMS
1361            );
1362
1363            $posts = $this->librarian->query_most_recent_posts( $item_limit );
1364            if ( empty( $posts ) ) {
1365                $buffer->append( array( 'url' => array( 'loc' => home_url( '/' ) ) ) );
1366            } else {
1367                foreach ( $posts as $post ) {
1368                    $current_item = $this->post_to_news_sitemap_item( $post );
1369
1370                    if ( $current_item['xml'] !== null && false === $buffer->append( $current_item['xml'] ) ) {
1371                        break;
1372                    }
1373                }
1374            }
1375
1376            if ( $this->logger ) {
1377                $this->logger->time( 'End news sitemap generation.' );
1378            }
1379
1380            $the_stored_news_sitemap = $buffer->contents();
1381
1382            set_transient(
1383                'jetpack_news_sitemap_xml',
1384                $the_stored_news_sitemap,
1385                JP_NEWS_SITEMAP_INTERVAL
1386            );
1387        } // End if.
1388
1389        return $the_stored_news_sitemap;
1390    }
1391
1392    /**
1393     * Construct the sitemap url entry for a WP_Post.
1394     *
1395     * @link https://www.sitemaps.org/protocol.html#urldef
1396     * @access private
1397     * @since 4.8.0
1398     *
1399     * @param object $post The post to be processed. Similar to WP_Post, but without post_content and post_content_filtered.
1400     *
1401     * @return array
1402     *              @type array  $xml An XML fragment representing the post URL.
1403     *              @type string $last_modified Date post was last modified.
1404     */
1405    private function post_to_sitemap_item( $post ) {
1406
1407        /**
1408         * Filter condition to allow skipping specific posts in sitemap.
1409         *
1410         * @module sitemaps
1411         *
1412         * @since 3.9.0
1413         *
1414         * @param bool   $skip Current boolean. False by default, so no post is skipped.
1415         * @param object $post Current post in the form of a $wpdb result object. Not WP_Post.
1416         *                     Doesn't have all the properties of a WP_Post.
1417         */
1418        if ( true === apply_filters( 'jetpack_sitemap_skip_post', false, $post ) ) {
1419            return array(
1420                'xml'           => null,
1421                'last_modified' => null,
1422            );
1423        }
1424
1425        $url = esc_url( get_permalink( $post ) );
1426
1427        /*
1428         * Spec requires the URL to be <=2048 bytes.
1429         * In practice this constraint is unlikely to be violated.
1430         */
1431        if ( 2048 < strlen( $url ) ) {
1432            $url = home_url() . '/?p=' . $post->ID;
1433        }
1434
1435        $last_modified = $post->post_modified_gmt;
1436
1437        // Check for more recent comments.
1438        // Note that 'Y-m-d h:i:s' strings sort lexicographically.
1439        if ( 0 < $post->comment_count ) {
1440            $last_modified = max(
1441                $last_modified,
1442                $this->librarian->query_latest_approved_comment_time_on_post( $post->ID )
1443            );
1444        }
1445
1446        $item_array = array(
1447            'url' => array(
1448                'loc'     => $url,
1449                'lastmod' => jp_sitemap_datetime( $last_modified ),
1450            ),
1451        );
1452
1453        /**
1454         * Filter sitemap URL item before rendering it as XML.
1455         *
1456         * @module sitemaps
1457         *
1458         * @since 3.9.0
1459         *
1460         * @param array $tree Associative array representing sitemap URL element.
1461         * @param int   $post_id ID of the post being processed.
1462         */
1463        $item_array = apply_filters( 'jetpack_sitemap_url', $item_array, $post->ID );
1464
1465        return array(
1466            'xml'           => $item_array,
1467            'last_modified' => $last_modified,
1468        );
1469    }
1470
1471    /**
1472     * Construct the image sitemap url entry for a WP_Post of image type.
1473     *
1474     * @link https://www.sitemaps.org/protocol.html#urldef
1475     *
1476     * @access private
1477     * @since 4.8.0
1478     *
1479     * @param WP_Post $post The image post to be processed.
1480     *
1481     * @return array
1482     *              @type array  $xml An XML fragment representing the post URL.
1483     *              @type string $last_modified Date post was last modified.
1484     */
1485    private function image_post_to_sitemap_item( $post ) {
1486
1487        /**
1488         * Filter condition to allow skipping specific image posts in the sitemap.
1489         *
1490         * @module sitemaps
1491         *
1492         * @since 4.8.0
1493         *
1494         * @param bool    $skip Current boolean. False by default, so no post is skipped.
1495         * @param WP_POST $post Current post object.
1496         */
1497        if ( apply_filters( 'jetpack_sitemap_image_skip_post', false, $post ) ) {
1498            return array(
1499                'xml'           => null,
1500                'last_modified' => null,
1501            );
1502        }
1503
1504        $url = wp_get_attachment_url( $post->ID );
1505
1506        // Do not include the image if the attached parent is not published.
1507        // Unattached will be published. Otherwise, will inherit parent status.
1508        if ( 'publish' !== get_post_status( $post ) ) {
1509            return array(
1510                'xml'           => null,
1511                'last_modified' => null,
1512            );
1513        }
1514
1515        $parent_url = get_permalink( get_post( $post->post_parent ) );
1516        if ( '' == $parent_url ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- WPCS: loose comparison ok.
1517            $parent_url = get_permalink( $post );
1518        }
1519
1520        $item_array = array(
1521            'url' => array(
1522                'loc'         => $parent_url,
1523                'lastmod'     => jp_sitemap_datetime( $post->post_modified_gmt ),
1524                'image:image' => array(
1525                    'image:loc' => $url,
1526                ),
1527            ),
1528        );
1529
1530        /**
1531         * Filter associative array with data to build <url> node
1532         * and its descendants for current post in image sitemap.
1533         *
1534         * @module sitemaps
1535         *
1536         * @since 4.8.0
1537         *
1538         * @param array $item_array Data to build parent and children nodes for current post.
1539         * @param int   $post_id Current image post ID.
1540         */
1541        $item_array = apply_filters(
1542            'jetpack_sitemap_image_sitemap_item',
1543            $item_array,
1544            $post->ID
1545        );
1546
1547        return array(
1548            'xml'           => $item_array,
1549            'last_modified' => $post->post_modified_gmt,
1550        );
1551    }
1552
1553    /**
1554     * Construct the video sitemap url entry for a WP_Post of video type.
1555     *
1556     * @link https://www.sitemaps.org/protocol.html#urldef
1557     * @link https://developers.google.com/webmasters/videosearch/sitemaps
1558     *
1559     * @access private
1560     * @since 4.8.0
1561     *
1562     * @param WP_Post $post The video post to be processed.
1563     *
1564     * @return array
1565     *              @type array  $xml An XML fragment representing the post URL.
1566     *              @type string $last_modified Date post was last modified.
1567     */
1568    private function video_post_to_sitemap_item( $post ) {
1569
1570        /**
1571         * Filter condition to allow skipping specific video posts in the sitemap.
1572         *
1573         * @module sitemaps
1574         *
1575         * @since 4.8.0
1576         *
1577         * @param bool    $skip Current boolean. False by default, so no post is skipped.
1578         * @param WP_POST $post Current post object.
1579         */
1580        if ( apply_filters( 'jetpack_sitemap_video_skip_post', false, $post ) ) {
1581            return array(
1582                'xml'           => null,
1583                'last_modified' => null,
1584            );
1585        }
1586
1587        // Do not include the video if the attached parent is not published.
1588        // Unattached will be published. Otherwise, will inherit parent status.
1589        if ( 'publish' !== get_post_status( $post ) ) {
1590            return array(
1591                'xml'           => null,
1592                'last_modified' => null,
1593            );
1594        }
1595
1596        $parent_url = esc_url( get_permalink( get_post( $post->post_parent ) ) );
1597        if ( '' == $parent_url ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- WPCS: loose comparison ok.
1598            $parent_url = esc_url( get_permalink( $post ) );
1599        }
1600
1601        // Prepare the content like get_the_content_feed().
1602        $content = $post->post_content;
1603        /** This filter is already documented in core/wp-includes/post-template.php */
1604        $content = apply_filters( 'the_content', $content );
1605
1606        /** This filter is already documented in core/wp-includes/feed.php */
1607        $content = apply_filters( 'the_content_feed', $content, 'rss2' );
1608
1609        // Include thumbnails for VideoPress videos, use blank image for others.
1610        if ( 'complete' === get_post_meta( $post->ID, 'videopress_status', true ) && has_post_thumbnail( $post ) ) {
1611            $video_thumbnail_url = get_the_post_thumbnail_url( $post );
1612        } else {
1613            /**
1614             * Filter the thumbnail image used in the video sitemap for non-VideoPress videos.
1615             *
1616             * @since 7.2.0
1617             *
1618             * @param string $str Image URL.
1619             */
1620            $video_thumbnail_url = apply_filters( 'jetpack_video_sitemap_default_thumbnail', 'https://s0.wp.com/i/blank.jpg' );
1621        }
1622
1623        $item_array = array(
1624            'url' => array(
1625                'loc'         => $parent_url,
1626                'lastmod'     => jp_sitemap_datetime( $post->post_modified_gmt ),
1627                'video:video' => array(
1628                    /** This filter is already documented in core/wp-includes/feed.php */
1629                    'video:title'         => apply_filters( 'the_title_rss', $post->post_title ),
1630                    'video:thumbnail_loc' => esc_url( $video_thumbnail_url ),
1631                    'video:description'   => $content,
1632                    'video:content_loc'   => esc_url( wp_get_attachment_url( $post->ID ) ),
1633                ),
1634            ),
1635        );
1636
1637        // TODO: Integrate with VideoPress here.
1638        // cf. video:player_loc tag in video sitemap spec.
1639
1640        /**
1641         * Filter associative array with data to build <url> node
1642         * and its descendants for current post in video sitemap.
1643         *
1644         * @module sitemaps
1645         *
1646         * @since 4.8.0
1647         *
1648         * @param array $item_array Data to build parent and children nodes for current post.
1649         * @param int   $post_id Current video post ID.
1650         */
1651        $item_array = apply_filters(
1652            'jetpack_sitemap_video_sitemap_item',
1653            $item_array,
1654            $post->ID
1655        );
1656
1657        return array(
1658            'xml'           => $item_array,
1659            'last_modified' => $post->post_modified_gmt,
1660        );
1661    }
1662
1663    /**
1664     * Construct the news sitemap url entry for a WP_Post.
1665     *
1666     * @link https://www.sitemaps.org/protocol.html#urldef
1667     *
1668     * @access private
1669     * @since 4.8.0
1670     *
1671     * @param object $post The post to be processed. Similar to WP_Post, but without post_content and post_content_filtered.
1672     *
1673     * @return string An XML fragment representing the post URL.
1674     */
1675    private function post_to_news_sitemap_item( $post ) {
1676
1677        // Exclude posts with meta 'jetpack_seo_noindex' set true from the Jetpack news sitemap.
1678        add_filter( 'jetpack_sitemap_news_skip_post', array( 'Jetpack_SEO_Posts', 'exclude_noindex_posts_from_jetpack_sitemap' ), 10, 2 );
1679
1680        /**
1681         * Filter condition to allow skipping specific posts in news sitemap.
1682         *
1683         * @module sitemaps
1684         *
1685         * @since 3.9.0
1686         *
1687         * @param bool   $skip Current boolean. False by default, so no post is skipped.
1688         * @param object $post Current post in the form of a $wpdb result object. Not WP_Post.
1689         *                     Doesn't have all the properties of a WP_Post.
1690         */
1691        if ( apply_filters( 'jetpack_sitemap_news_skip_post', false, $post ) ) {
1692            return array(
1693                'xml' => null,
1694            );
1695        }
1696
1697        $url = get_permalink( $post );
1698
1699        /*
1700         * Spec requires the URL to be <=2048 bytes.
1701         * In practice this constraint is unlikely to be violated.
1702         */
1703        if ( 2048 < strlen( $url ) ) {
1704            $url = home_url() . '/?p=' . $post->ID;
1705        }
1706
1707        /*
1708         * Trim the locale to an ISO 639 language code as required by Google.
1709         * Special cases are zh-cn (Simplified Chinese) and zh-tw (Traditional Chinese).
1710         * @link https://www.loc.gov/standards/iso639-2/php/code_list.php
1711         */
1712        $language = strtolower( get_locale() );
1713
1714        if ( in_array( $language, array( 'zh_tw', 'zh_cn' ), true ) ) {
1715            $language = str_replace( '_', '-', $language );
1716        } else {
1717            $language = preg_replace( '/(_.*)$/i', '', $language );
1718        }
1719
1720        $item_array = array(
1721            'url' => array(
1722                'loc'       => $url,
1723                'lastmod'   => jp_sitemap_datetime( $post->post_modified_gmt ),
1724                'news:news' => array(
1725                    'news:publication'      => array(
1726                        'news:name'     => html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ),
1727                        'news:language' => $language,
1728                    ),
1729                    /** This filter is already documented in core/wp-includes/feed.php */
1730                    'news:title'            => apply_filters( 'the_title_rss', $post->post_title ),
1731                    'news:publication_date' => jp_sitemap_datetime( $post->post_date_gmt ),
1732                    'news:genres'           => 'Blog',
1733                ),
1734            ),
1735        );
1736
1737        /**
1738         * Filter associative array with data to build <url> node
1739         * and its descendants for current post in news sitemap.
1740         *
1741         * @module sitemaps
1742         *
1743         * @since 3.9.0
1744         *
1745         * @param array $item_array Data to build parent and children nodes for current post.
1746         * @param int   $post_id Current post ID.
1747         */
1748        $item_array = apply_filters(
1749            'jetpack_sitemap_news_sitemap_item',
1750            $item_array,
1751            $post->ID
1752        );
1753
1754        return array(
1755            'xml' => $item_array,
1756        );
1757    }
1758}