PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.4
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.4
28.6 28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 All 130 releases
wordpress-seo / inc / sitemaps / class-post-type-sitemap-provider.php

class-post-type-sitemap-provider.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.4, at inc/sitemaps/class-post-type-sitemap-provider.php

785 lines 20.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WPSEO plugin file.
4 *
5 * @package WPSEO\XML_Sitemaps
6 */
7
8 use Yoast\WP\SEO\Models\SEO_Links;
9
10 /**
11 * Sitemap provider for author archives.
12 */
13 class WPSEO_Post_Type_Sitemap_Provider implements WPSEO_Sitemap_Provider {
14
15 /**
16 * Holds image parser instance.
17 *
18 * @var WPSEO_Sitemap_Image_Parser
19 */
20 protected static $image_parser;
21
22 /**
23 * Holds the parsed home url.
24 *
25 * @var array
26 */
27 protected static $parsed_home_url;
28
29 /**
30 * Determines whether images should be included in the XML sitemap.
31 *
32 * @var bool
33 */
34 private $include_images;
35
36 /**
37 * Set up object properties for data reuse.
38 */
39 public function __construct() {
40 add_action( 'save_post', [ $this, 'save_post' ] );
41
42 /**
43 * Filter - Allows excluding images from the XML sitemap.
44 *
45 * @param bool $include True to include, false to exclude.
46 */
47 $this->include_images = apply_filters( 'wpseo_xml_sitemap_include_images', true );
48 }
49
50 /**
51 * Get the Image Parser.
52 *
53 * @return WPSEO_Sitemap_Image_Parser
54 */
55 protected function get_image_parser() {
56 if ( ! isset( self::$image_parser ) ) {
57 self::$image_parser = new WPSEO_Sitemap_Image_Parser();
58 }
59
60 return self::$image_parser;
61 }
62
63 /**
64 * Gets the parsed home url.
65 *
66 * @return array The home url, as parsed by wp_parse_url.
67 */
68 protected function get_parsed_home_url() {
69 if ( ! isset( self::$parsed_home_url ) ) {
70 self::$parsed_home_url = wp_parse_url( home_url() );
71 }
72
73 return self::$parsed_home_url;
74 }
75
76 /**
77 * Check if provider supports given item type.
78 *
79 * @param string $type Type string to check for.
80 *
81 * @return bool
82 */
83 public function handles_type( $type ) {
84
85 return post_type_exists( $type );
86 }
87
88 /**
89 * Retrieves the sitemap links.
90 *
91 * @param int $max_entries Entries per sitemap.
92 *
93 * @return array
94 */
95 public function get_index_links( $max_entries ) {
96 global $wpdb;
97 $post_types = WPSEO_Post_Type::get_accessible_post_types();
98 $post_types = array_filter( $post_types, [ $this, 'is_valid_post_type' ] );
99 $last_modified_times = WPSEO_Sitemaps::get_last_modified_gmt( $post_types, true );
100 $index = [];
101
102 foreach ( $post_types as $post_type ) {
103
104 $total_count = $this->get_post_type_count( $post_type );
105
106 if ( $total_count === 0 ) {
107 continue;
108 }
109
110 $max_pages = 1;
111 if ( $total_count > $max_entries ) {
112 $max_pages = (int) ceil( $total_count / $max_entries );
113 }
114
115 $all_dates = [];
116
117 if ( $max_pages > 1 ) {
118 $all_dates = version_compare( $wpdb->db_version(), '8.0', '>=' ) ? $this->get_all_dates_using_with_clause( $post_type, $max_entries ) : $this->get_all_dates( $post_type, $max_entries );
119 }
120
121 for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) {
122
123 $current_page = ( $page_counter === 0 ) ? '' : ( $page_counter + 1 );
124 $date = false;
125
126 if ( empty( $current_page ) || $current_page === $max_pages ) {
127
128 if ( ! empty( $last_modified_times[ $post_type ] ) ) {
129 $date = $last_modified_times[ $post_type ];
130 }
131 }
132 else {
133 $date = $all_dates[ $page_counter ];
134 }
135
136 $index[] = [
137 'loc' => WPSEO_Sitemaps_Router::get_base_url( $post_type . '-sitemap' . $current_page . '.xml' ),
138 'lastmod' => $date,
139 ];
140 }
141 }
142
143 return $index;
144 }
145
146 /**
147 * Get set of sitemap link data.
148 *
149 * @param string $type Sitemap type.
150 * @param int $max_entries Entries per sitemap.
151 * @param int $current_page Current page of the sitemap.
152 *
153 * @return array
154 *
155 * @throws OutOfBoundsException When an invalid page is requested.
156 */
157 public function get_sitemap_links( $type, $max_entries, $current_page ) {
158
159 $links = [];
160 $post_type = $type;
161
162 if ( ! $this->is_valid_post_type( $post_type ) ) {
163 throw new OutOfBoundsException( 'Invalid sitemap page requested' );
164 }
165
166 $steps = min( 100, $max_entries );
167 $offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0;
168 $total = ( $offset + $max_entries );
169
170 $post_type_entries = $this->get_post_type_count( $post_type );
171
172 if ( $total > $post_type_entries ) {
173 $total = $post_type_entries;
174 }
175
176 if ( $current_page === 1 ) {
177 $links = array_merge( $links, $this->get_first_links( $post_type ) );
178 }
179
180 // If total post type count is lower than the offset, an invalid page is requested.
181 if ( $post_type_entries < $offset ) {
182 throw new OutOfBoundsException( 'Invalid sitemap page requested' );
183 }
184
185 if ( $post_type_entries === 0 ) {
186 return $links;
187 }
188
189 $posts_to_exclude = $this->get_excluded_posts( $type );
190
191 while ( $total > $offset ) {
192
193 $posts = $this->get_posts( $post_type, $steps, $offset );
194
195 $offset += $steps;
196
197 if ( empty( $posts ) ) {
198 continue;
199 }
200
201 foreach ( $posts as $post ) {
202
203 if ( in_array( $post->ID, $posts_to_exclude, true ) ) {
204 continue;
205 }
206
207 if ( WPSEO_Meta::get_value( 'meta-robots-noindex', $post->ID ) === '1' ) {
208 continue;
209 }
210
211 $url = $this->get_url( $post );
212
213 if ( ! isset( $url['loc'] ) ) {
214 continue;
215 }
216
217 /**
218 * Filter URL entry before it gets added to the sitemap.
219 *
220 * @param array $url Array of URL parts.
221 * @param string $type URL type.
222 * @param object $post Data object for the URL.
223 */
224 $url = apply_filters( 'wpseo_sitemap_entry', $url, 'post', $post );
225
226 if ( ! empty( $url ) ) {
227 $links[] = $url;
228 }
229 }
230
231 unset( $post, $url );
232 }
233
234 return $links;
235 }
236
237 /**
238 * Check for relevant post type before invalidation.
239 *
240 * @param int $post_id Post ID to possibly invalidate for.
241 *
242 * @return void
243 */
244 public function save_post( $post_id ) {
245
246 if ( $this->is_valid_post_type( get_post_type( $post_id ) ) ) {
247 WPSEO_Sitemaps_Cache::invalidate_post( $post_id );
248 }
249 }
250
251 /**
252 * Check if post type should be present in sitemaps.
253 *
254 * @param string $post_type Post type string to check for.
255 *
256 * @return bool
257 */
258 public function is_valid_post_type( $post_type ) {
259 if ( ! WPSEO_Post_Type::is_post_type_accessible( $post_type ) || ! WPSEO_Post_Type::is_post_type_indexable( $post_type ) ) {
260 return false;
261 }
262
263 /**
264 * Filter decision if post type is excluded from the XML sitemap.
265 *
266 * @param bool $exclude Default false.
267 * @param string $post_type Post type name.
268 */
269 if ( apply_filters( 'wpseo_sitemap_exclude_post_type', false, $post_type ) ) {
270 return false;
271 }
272
273 return true;
274 }
275
276 /**
277 * Retrieves a list with the excluded post ids.
278 *
279 * @param string $post_type Post type.
280 *
281 * @return array Array with post ids to exclude.
282 */
283 protected function get_excluded_posts( $post_type ) {
284 $excluded_posts_ids = [];
285
286 $page_on_front_id = ( $post_type === 'page' ) ? (int) get_option( 'page_on_front' ) : 0;
287 if ( $page_on_front_id > 0 ) {
288 $excluded_posts_ids[] = $page_on_front_id;
289 }
290
291 /**
292 * Filter: 'wpseo_exclude_from_sitemap_by_post_ids' - Allow extending and modifying the posts to exclude.
293 *
294 * @param array $posts_to_exclude The posts to exclude.
295 */
296 $excluded_posts_ids = apply_filters( 'wpseo_exclude_from_sitemap_by_post_ids', $excluded_posts_ids );
297 if ( ! is_array( $excluded_posts_ids ) ) {
298 $excluded_posts_ids = [];
299 }
300
301 $excluded_posts_ids = array_map( 'intval', $excluded_posts_ids );
302
303 $page_for_posts_id = ( $post_type === 'page' ) ? (int) get_option( 'page_for_posts' ) : 0;
304 if ( $page_for_posts_id > 0 ) {
305 $excluded_posts_ids[] = $page_for_posts_id;
306 }
307
308 return array_unique( $excluded_posts_ids );
309 }
310
311 /**
312 * Get count of posts for post type.
313 *
314 * @param string $post_type Post type to retrieve count for.
315 *
316 * @return int
317 */
318 protected function get_post_type_count( $post_type ) {
319
320 global $wpdb;
321
322 /**
323 * Filter JOIN query part for type count of post type.
324 *
325 * @param string $join SQL part, defaults to empty string.
326 * @param string $post_type Post type name.
327 */
328 $join_filter = apply_filters( 'wpseo_typecount_join', '', $post_type );
329
330 /**
331 * Filter WHERE query part for type count of post type.
332 *
333 * @param string $where SQL part, defaults to empty string.
334 * @param string $post_type Post type name.
335 */
336 $where_filter = apply_filters( 'wpseo_typecount_where', '', $post_type );
337
338 $where = $this->get_sql_where_clause( $post_type );
339
340 $sql = "
341 SELECT COUNT({$wpdb->posts}.ID)
342 FROM {$wpdb->posts}
343 {$join_filter}
344 {$where}
345 {$where_filter}
346 ";
347
348 return (int) $wpdb->get_var( $sql );
349 }
350
351 /**
352 * Produces set of links to prepend at start of first sitemap page.
353 *
354 * @param string $post_type Post type to produce links for.
355 *
356 * @return array
357 */
358 protected function get_first_links( $post_type ) {
359
360 $links = [];
361 $archive_url = false;
362
363 if ( $post_type === 'page' ) {
364
365 $page_on_front_id = (int) get_option( 'page_on_front' );
366 if ( $page_on_front_id > 0 ) {
367 $front_page = $this->get_url(
368 get_post( $page_on_front_id ),
369 );
370 }
371
372 if ( empty( $front_page ) ) {
373 $front_page = [
374 'loc' => YoastSEO()->helpers->url->home(),
375 ];
376 }
377
378 // Deprecated, kept for backwards data compat. R.
379 $front_page['chf'] = 'daily';
380 $front_page['pri'] = 1;
381
382 $images = ( $front_page['images'] ?? [] );
383
384 /**
385 * Filter images to be included for the term in XML sitemap.
386 *
387 * @param array $images Array of image items.
388 * @return array $image_list Array of image items.
389 */
390 $image_list = apply_filters( 'wpseo_sitemap_urlimages_front_page', $images );
391 if ( is_array( $image_list ) ) {
392 $front_page['images'] = $image_list;
393 }
394
395 $links[] = $front_page;
396 }
397 elseif ( $post_type !== 'page' ) {
398 /**
399 * Filter the URL Yoast SEO uses in the XML sitemap for this post type archive.
400 *
401 * @param string $archive_url The URL of this archive
402 * @param string $post_type The post type this archive is for.
403 */
404 $archive_url = apply_filters(
405 'wpseo_sitemap_post_type_archive_link',
406 $this->get_post_type_archive_link( $post_type ),
407 $post_type,
408 );
409 }
410
411 if ( $archive_url ) {
412
413 $links[] = [
414 'loc' => $archive_url,
415 'mod' => WPSEO_Sitemaps::get_last_modified_gmt( $post_type ),
416
417 // Deprecated, kept for backwards data compat. R.
418 'chf' => 'daily',
419 'pri' => 1,
420 ];
421 }
422
423 /**
424 * Filters the first post type links.
425 *
426 * @param array $links The first post type links.
427 * @param string $post_type The post type this archive is for.
428 */
429 return apply_filters( 'wpseo_sitemap_post_type_first_links', $links, $post_type );
430 }
431
432 /**
433 * Get URL for a post type archive.
434 *
435 * @since 5.3
436 *
437 * @param string $post_type Post type.
438 *
439 * @return string|bool URL or false if it should be excluded.
440 */
441 protected function get_post_type_archive_link( $post_type ) {
442
443 $pt_archive_page_id = -1;
444
445 if ( $post_type === 'post' ) {
446
447 if ( get_option( 'show_on_front' ) === 'posts' ) {
448 return YoastSEO()->helpers->url->home();
449 }
450
451 $pt_archive_page_id = (int) get_option( 'page_for_posts' );
452
453 // Post archive should be excluded if posts page isn't set.
454 if ( $pt_archive_page_id <= 0 ) {
455 return false;
456 }
457 }
458
459 if ( ! $this->is_post_type_archive_indexable( $post_type, $pt_archive_page_id ) ) {
460 return false;
461 }
462
463 return get_post_type_archive_link( $post_type );
464 }
465
466 /**
467 * Determines whether a post type archive is indexable.
468 *
469 * @since 11.5
470 *
471 * @param string $post_type Post type.
472 * @param int $archive_page_id The page id.
473 *
474 * @return bool True when post type archive is indexable.
475 */
476 protected function is_post_type_archive_indexable( $post_type, $archive_page_id = -1 ) {
477
478 if ( WPSEO_Options::get( 'noindex-ptarchive-' . $post_type, false ) ) {
479 return false;
480 }
481
482 /**
483 * Filter the page which is dedicated to this post type archive.
484 *
485 * @since 9.3
486 *
487 * @param string $archive_page_id The post_id of the page.
488 * @param string $post_type The post type this archive is for.
489 */
490 $archive_page_id = (int) apply_filters( 'wpseo_sitemap_page_for_post_type_archive', $archive_page_id, $post_type );
491
492 if ( $archive_page_id > 0 && WPSEO_Meta::get_value( 'meta-robots-noindex', $archive_page_id ) === '1' ) {
493 return false;
494 }
495
496 return true;
497 }
498
499 /**
500 * Retrieve set of posts with optimized query routine.
501 *
502 * @param string $post_type Post type to retrieve.
503 * @param int $count Count of posts to retrieve.
504 * @param int $offset Starting offset.
505 *
506 * @return object[]
507 */
508 protected function get_posts( $post_type, $count, $offset ) {
509
510 global $wpdb;
511
512 static $filters = [];
513
514 if ( ! isset( $filters[ $post_type ] ) ) {
515 // Make sure you're wpdb->preparing everything you throw into this!!
516 $filters[ $post_type ] = [
517 /**
518 * Filter JOIN query part for the post type.
519 *
520 * @param string $join SQL part, defaults to false.
521 * @param string $post_type Post type name.
522 */
523 'join' => apply_filters( 'wpseo_posts_join', false, $post_type ),
524
525 /**
526 * Filter WHERE query part for the post type.
527 *
528 * @param string $where SQL part, defaults to false.
529 * @param string $post_type Post type name.
530 */
531 'where' => apply_filters( 'wpseo_posts_where', false, $post_type ),
532 ];
533 }
534
535 $join_filter = $filters[ $post_type ]['join'];
536 $where_filter = $filters[ $post_type ]['where'];
537 $where = $this->get_sql_where_clause( $post_type );
538
539 /*
540 * Optimized query per this thread:
541 * {@link http://wordpress.org/support/topic/plugin-wordpress-seo-by-yoast-performance-suggestion}.
542 * Also see {@link http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/}.
543 */
544 $sql = "
545 SELECT l.ID, post_title, post_content, post_name, post_parent, post_author, post_status, post_modified_gmt, post_date, post_date_gmt
546 FROM (
547 SELECT {$wpdb->posts}.ID
548 FROM {$wpdb->posts}
549 {$join_filter}
550 {$where}
551 {$where_filter}
552 ORDER BY {$wpdb->posts}.post_modified ASC LIMIT %d OFFSET %d
553 )
554 o JOIN {$wpdb->posts} l ON l.ID = o.ID
555 ";
556
557 $posts = $wpdb->get_results( $wpdb->prepare( $sql, $count, $offset ) );
558
559 $post_ids = [];
560
561 foreach ( $posts as $post_index => $post ) {
562 $post->post_type = $post_type;
563 $sanitized_post = sanitize_post( $post, 'raw' );
564 $posts[ $post_index ] = new WP_Post( $sanitized_post );
565
566 $post_ids[] = $sanitized_post->ID;
567 }
568
569 /**
570 * Filter to disable priming the post, term and featured-image caches for the sitemap.
571 *
572 * @since 28.3
573 *
574 * @param bool $disable_cache_priming Whether to disable priming the caches. Defaults to false.
575 */
576 $disable_priming = apply_filters( 'wpseo_disable_xml_sitemap_cache_priming', false );
577
578 if ( ! $disable_priming ) {
579 // Warm the post and term caches in bulk, so permalink and image building doesn't query per post.
580 _prime_post_caches( $post_ids, true, false );
581 }
582
583 update_meta_cache( 'post', $post_ids );
584
585 if ( ! $disable_priming && $this->include_images ) {
586 $this->get_image_parser()->prime_thumbnail_caches( $posts );
587 }
588
589 return $posts;
590 }
591
592 /**
593 * Constructs an SQL where clause for a given post type.
594 *
595 * @param string $post_type Post type slug.
596 *
597 * @return string
598 */
599 protected function get_sql_where_clause( $post_type ) {
600
601 global $wpdb;
602
603 $join = '';
604 $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) );
605 $status_where = "{$wpdb->posts}.post_status IN ('" . implode( "','", $post_statuses ) . "')";
606
607 // Based on WP_Query->get_posts(). R.
608 if ( $post_type === 'attachment' ) {
609 $join = " LEFT JOIN {$wpdb->posts} AS p2 ON ({$wpdb->posts}.post_parent = p2.ID) ";
610 $parent_statuses = array_diff( $post_statuses, [ 'inherit' ] );
611 $status_where = "p2.post_status IN ('" . implode( "','", $parent_statuses ) . "') AND p2.post_password = ''";
612 }
613
614 $where_clause = "
615 {$join}
616 WHERE {$status_where}
617 AND {$wpdb->posts}.post_type = %s
618 AND {$wpdb->posts}.post_password = ''
619 AND {$wpdb->posts}.post_date != '0000-00-00 00:00:00'
620 ";
621
622 return $wpdb->prepare( $where_clause, $post_type );
623 }
624
625 /**
626 * Produce array of URL parts for given post object.
627 *
628 * @param object $post Post object to get URL parts for.
629 *
630 * @return array|bool
631 */
632 protected function get_url( $post ) {
633
634 $url = [];
635
636 /**
637 * Filter the URL Yoast SEO uses in the XML sitemap.
638 *
639 * Note that only absolute local URLs are allowed as the check after this removes external URLs.
640 *
641 * @param string $url URL to use in the XML sitemap
642 * @param object $post Post object for the URL.
643 */
644 $url['loc'] = apply_filters( 'wpseo_xml_sitemap_post_url', get_permalink( $post ), $post );
645 $link_type = YoastSEO()->helpers->url->get_link_type(
646 wp_parse_url( $url['loc'] ),
647 $this->get_parsed_home_url(),
648 );
649
650 /*
651 * Do not include external URLs.
652 *
653 * {@link https://wordpress.org/plugins/page-links-to/} can rewrite permalinks to external URLs.
654 */
655 if ( $link_type === SEO_Links::TYPE_EXTERNAL ) {
656 return false;
657 }
658
659 $modified = max( $post->post_modified_gmt, $post->post_date_gmt );
660
661 if ( $modified !== '0000-00-00 00:00:00' ) {
662 $url['mod'] = $modified;
663 }
664
665 $url['chf'] = 'daily'; // Deprecated, kept for backwards data compat. R.
666
667 $canonical = WPSEO_Meta::get_value( 'canonical', $post->ID );
668
669 if ( $canonical !== '' && $canonical !== $url['loc'] ) {
670 /*
671 * Let's assume that if a canonical is set for this page and it's different from
672 * the URL of this post, that page is either already in the XML sitemap OR is on
673 * an external site, either way, we shouldn't include it here.
674 */
675 return false;
676 }
677 unset( $canonical );
678
679 $url['pri'] = 1; // Deprecated, kept for backwards data compat. R.
680
681 if ( $this->include_images ) {
682 $url['images'] = $this->get_image_parser()->get_images( $post );
683 }
684
685 return $url;
686 }
687
688 /**
689 * Get all dates for a post type by using the WITH clause for performance.
690 *
691 * @param string $post_type Post type to retrieve dates for.
692 * @param int $max_entries Maximum number of entries to retrieve.
693 *
694 * @return array Array of dates.
695 */
696 private function get_all_dates_using_with_clause( $post_type, $max_entries ) {
697 global $wpdb;
698
699 $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) );
700
701 $replacements = array_merge(
702 [
703 'ordering',
704 'post_modified_gmt',
705 $wpdb->posts,
706 'type_status_date',
707 'post_status',
708 ],
709 $post_statuses,
710 [
711 'post_type',
712 $post_type,
713 'post_modified_gmt',
714 'post_modified_gmt',
715 'ordering',
716 $max_entries,
717 ],
718 );
719
720 //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here.
721 //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
722 return $wpdb->get_col(
723 //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized.
724 $wpdb->prepare(
725 '
726 WITH %i AS (SELECT ROW_NUMBER() OVER (ORDER BY %i) AS n, post_modified_gmt
727 FROM %i USE INDEX ( %i )
728 WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ')
729 AND %i = %s
730 ORDER BY %i)
731 SELECT %i
732 FROM %i
733 WHERE MOD(n, %d) = 0;
734 ',
735 $replacements,
736 ),
737 );
738 }
739
740 /**
741 * Get all dates for a post type.
742 *
743 * @param string $post_type Post type to retrieve dates for.
744 * @param int $max_entries Maximum number of entries to retrieve.
745 *
746 * @return array Array of dates.
747 */
748 private function get_all_dates( $post_type, $max_entries ) {
749 global $wpdb;
750
751 $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) );
752 $replacements = array_merge(
753 [
754 'post_modified_gmt',
755 $wpdb->posts,
756 'type_status_date',
757 'post_status',
758 ],
759 $post_statuses,
760 [
761 'post_type',
762 $post_type,
763 $max_entries,
764 'post_modified_gmt',
765 ],
766 );
767
768 return $wpdb->get_col(
769 //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized.
770 $wpdb->prepare(
771 '
772 SELECT %i
773 FROM ( SELECT @rownum:=0 ) init
774 JOIN %i USE INDEX( %i )
775 WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ')
776 AND %i = %s
777 AND ( @rownum:=@rownum+1 ) %% %d = 0
778 ORDER BY %i ASC
779 ',
780 $replacements,
781 ),
782 );
783 }
784 }
785