PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | modules/sitemaps/sitemap-builder.php +1758 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,1758 @@
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 +
10 +if ( ! defined( 'ABSPATH' ) ) {
11 + exit( 0 );
12 +}
13 +
14 +/* Include sitemap subclasses, if not already, and include proper buffer based on phpxml's availability. */
15 +require_once __DIR__ . '/sitemap-constants.php';
16 +require_once __DIR__ . '/sitemap-buffer.php';
17 +
18 +if ( ! 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 +
33 +require_once __DIR__ . '/sitemap-librarian.php';
34 +require_once __DIR__ . '/sitemap-finder.php';
35 +require_once __DIR__ . '/sitemap-state.php';
36 +
37 +if ( 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 + */
44 +class 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 + */
90 +class 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 +}