PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 All 502 releases
jetpack / modules / sitemaps / sitemap-builder.php

sitemap-builder.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at modules/sitemaps/sitemap-builder.php

1,759 lines 48.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
1759