PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Indexable / Comment / Comment.php

Comment.php in ElasticPress 5.3.5, at includes/classes/Indexable/Comment/Comment.php

1,222 lines 32.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Comment indexable
4 *
5 * @since 3.6.0
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Indexable\Comment;
10
11 use WP_Comment_Query;
12 use ElasticPress\Elasticsearch;
13 use ElasticPress\Features;
14 use ElasticPress\Indexable;
15 use ElasticPress\Indexable\Post\DateQuery;
16 use ElasticPress\Indexables;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit; // Exit if accessed directly.
20 }
21
22 /**
23 * Comment indexable class
24 */
25 class Comment extends Indexable {
26
27 /**
28 * Indexable slug
29 *
30 * @var string
31 * @since 3.6.0
32 */
33 public $slug = 'comment';
34
35 /**
36 * Flag to indicate if the indexable has support for
37 * `id_range` pagination method during a sync.
38 *
39 * @var boolean
40 * @since 5.2.0
41 */
42 public $support_indexing_advanced_pagination = true;
43
44 /**
45 * Instantiate the indexable SyncManager and QueryIntegration, the main responsibles for the WP integration.
46 *
47 * @since 4.5.0
48 * @return void
49 */
50 public function setup() {
51 $this->labels = [
52 'plural' => esc_html__( 'Comments', 'elasticpress' ),
53 'singular' => esc_html__( 'Comment', 'elasticpress' ),
54 ];
55
56 $this->sync_manager = new SyncManager( $this->slug );
57 $this->query_integration = new QueryIntegration();
58 }
59
60 /**
61 * Format query vars into ES query
62 *
63 * @param array $query_vars WP_Comment_Query args.
64 * @since 3.6.0
65 * @return array
66 */
67 public function format_args( $query_vars ) {
68 /**
69 * Support `number` query var
70 */
71 if ( ! empty( $query_vars['number'] ) ) {
72 $number = (int) $query_vars['number'];
73 } else {
74 /**
75 * Set the maximum results window size.
76 *
77 * The request will return a HTTP 500 Internal Error if the size of the
78 * request is larger than the [index.max_result_window] parameter in ES.
79 * See the scroll api for a more efficient way to request large data sets.
80 *
81 * @return int The max results window size.
82 *
83 * @since 2.3.0
84 */
85 $number = apply_filters( 'ep_max_results_window', 10000 );
86 }
87
88 $formatted_args = [
89 'from' => 0,
90 'size' => $number,
91 ];
92
93 /**
94 * Support `offset` query var
95 */
96 if ( isset( $query_vars['offset'] ) ) {
97 $formatted_args['from'] = (int) $query_vars['offset'];
98 if ( empty( $query_vars['number'] ) ) {
99 $formatted_args['size'] -= (int) $formatted_args['from'];
100 }
101 }
102
103 /**
104 * Support `paged` query var
105 *
106 * If `offset` is used, that takes precedence
107 * over this.
108 */
109 if ( isset( $query_vars['paged'] ) && empty( $query_vars['offset'] ) && $query_vars['paged'] > 1 ) {
110 $formatted_args['from'] = $number * ( $query_vars['paged'] - 1 );
111 }
112
113 /**
114 * Support `order` and `orderby` query vars
115 */
116
117 // Set sort order, default is 'desc'.
118 if ( ! empty( $query_vars['order'] ) ) {
119 $order = $this->parse_order( $query_vars['order'] );
120 } else {
121 $order = 'desc';
122 }
123
124 // Default sort by comment date
125 if ( empty( $query_vars['orderby'] ) ) {
126 $query_vars['orderby'] = 'comment_date_gmt';
127 }
128
129 // Set sort type.
130 $formatted_args['sort'] = $this->parse_orderby( $query_vars['orderby'], $order, $query_vars );
131
132 $filter = [
133 'bool' => [
134 'must' => [],
135 ],
136 ];
137
138 $use_filters = false;
139
140 /**
141 * Support `author_email` query var
142 */
143 if ( ! empty( $query_vars['author_email'] ) ) {
144 $filter['bool']['must'][] = [
145 'term' => [
146 'comment_author_email.raw' => $query_vars['author_email'],
147 ],
148 ];
149
150 $use_filters = true;
151 }
152
153 /**
154 * Support `author_url` query var
155 */
156 if ( ! empty( $query_vars['author_url'] ) ) {
157 $filter['bool']['must'][] = [
158 'term' => [
159 'comment_author_url.raw' => $query_vars['author_url'],
160 ],
161 ];
162
163 $use_filters = true;
164 }
165
166 /**
167 * Support `user_id` query var
168 */
169 if ( ! empty( $query_vars['user_id'] ) ) {
170 $filter['bool']['must'][] = [
171 'term' => [
172 'user_id' => (int) $query_vars['user_id'],
173 ],
174 ];
175
176 $use_filters = true;
177 }
178
179 /**
180 * Support `author__in` query var
181 */
182 if ( ! empty( $query_vars['author__in'] ) ) {
183 $filter['bool']['must'][]['bool']['must'] = [
184 'terms' => [
185 'user_id' => array_values( (array) $query_vars['author__in'] ),
186 ],
187 ];
188
189 $use_filters = true;
190 }
191
192 /**
193 * Support `author__not_in` query var
194 */
195 if ( ! empty( $query_vars['author__not_in'] ) ) {
196 $filter['bool']['must'][]['bool']['must_not'] = [
197 'terms' => [
198 'user_id' => array_values( (array) $query_vars['author__not_in'] ),
199 ],
200 ];
201
202 $use_filters = true;
203 }
204
205 /**
206 * Support `comment__in` query var
207 */
208 if ( ! empty( $query_vars['comment__in'] ) ) {
209 $filter['bool']['must'][]['bool']['must'] = [
210 'terms' => [
211 'comment_ID' => array_values( (array) $query_vars['comment__in'] ),
212 ],
213 ];
214
215 $use_filters = true;
216 }
217
218 /**
219 * Support `comment__not_in` query var
220 */
221 if ( ! empty( $query_vars['comment__not_in'] ) ) {
222 $filter['bool']['must'][]['bool']['must_not'] = [
223 'terms' => [
224 'comment_ID' => array_values( (array) $query_vars['comment__not_in'] ),
225 ],
226 ];
227
228 $use_filters = true;
229 }
230
231 /**
232 * Support `date_query` query var
233 */
234 if ( ! empty( $query_vars['date_query'] ) ) {
235 $date_query = new DateQuery( $query_vars['date_query'] );
236 $date_filter = $date_query->get_es_filter();
237
238 if ( array_key_exists( 'and', $date_filter ) ) {
239 $filter['bool']['must'][] = $date_filter['and'];
240 $use_filters = true;
241 }
242 }
243
244 /**
245 * Support `fields` query var.
246 */
247 if ( isset( $query_vars['fields'] ) ) {
248 switch ( $query_vars['fields'] ) {
249 case 'ids':
250 $formatted_args['_source'] = [
251 'includes' => [
252 'comment_ID',
253 ],
254 ];
255 break;
256 }
257 }
258
259 /**
260 * Support `karma` query var.
261 */
262 if ( ! empty( $query_vars['karma'] ) || 0 === $query_vars['karma'] ) {
263 $filter['bool']['must'][]['bool']['must'] = [
264 'term' => [
265 'comment_karma' => $query_vars['karma'],
266 ],
267 ];
268
269 $use_filters = true;
270 }
271
272 $meta_queries = [];
273
274 /**
275 * Support `meta_key` and `meta_value` query args
276 */
277 if ( ! empty( $query_vars['meta_key'] ) ) {
278 $meta_query_array = [
279 'key' => $query_vars['meta_key'],
280 ];
281
282 if ( isset( $query_vars['meta_value'] ) ) {
283 $meta_query_array['value'] = $query_vars['meta_value'];
284 }
285
286 $meta_queries[] = $meta_query_array;
287 }
288
289 /**
290 * Support 'meta_query' query var.
291 */
292 if ( ! empty( $query_vars['meta_query'] ) ) {
293 $meta_queries = array_merge( $meta_queries, $query_vars['meta_query'] );
294 }
295
296 if ( ! empty( $meta_queries ) ) {
297 $built_meta_queries = $this->build_meta_query( $meta_queries );
298
299 if ( $built_meta_queries ) {
300 $filter['bool']['must'][] = $built_meta_queries;
301 $use_filters = true;
302 }
303 }
304
305 /**
306 * Support `hierarchical` query var
307 */
308 if ( ! empty( $query_vars['hierarchical'] ) && empty( $query_vars['parent'] ) ) {
309 $query_vars['parent'] = 0;
310 }
311
312 /**
313 * Support `parent` query var.
314 */
315 if ( ! empty( $query_vars['parent'] ) || 0 === $query_vars['parent'] ) {
316 $filter['bool']['must'][]['bool']['must'] = [
317 'term' => [
318 'comment_parent' => (int) $query_vars['parent'],
319 ],
320 ];
321
322 $use_filters = true;
323 }
324
325 /**
326 * Support `parent__in` query var
327 */
328 if ( ! empty( $query_vars['parent__in'] ) ) {
329 $filter['bool']['must'][]['bool']['must'] = [
330 'terms' => [
331 'comment_parent' => array_values( (array) $query_vars['parent__in'] ),
332 ],
333 ];
334
335 $use_filters = true;
336 }
337
338 /**
339 * Support `parent__not_in` query var
340 */
341 if ( ! empty( $query_vars['parent__not_in'] ) ) {
342 $filter['bool']['must'][]['bool']['must_not'] = [
343 'terms' => [
344 'comment_parent' => array_values( (array) $query_vars['parent__not_in'] ),
345 ],
346 ];
347
348 $use_filters = true;
349 }
350
351 /**
352 * Support `post_author` query var.
353 */
354 if ( ! empty( $query_vars['post_author'] ) ) {
355 $filter['bool']['must'][]['bool']['must'] = [
356 'term' => [
357 'comment_post_author_ID' => (int) $query_vars['post_author'],
358 ],
359 ];
360
361 $use_filters = true;
362 }
363
364 /**
365 * Support `post_author__in` query var
366 */
367 if ( ! empty( $query_vars['post_author__in'] ) ) {
368 $filter['bool']['must'][]['bool']['must'] = [
369 'terms' => [
370 'comment_post_author_ID' => array_values( (array) $query_vars['post_author__in'] ),
371 ],
372 ];
373
374 $use_filters = true;
375 }
376
377 /**
378 * Support `post_author__not_in` query var
379 */
380 if ( ! empty( $query_vars['post_author__not_in'] ) ) {
381 $filter['bool']['must'][]['bool']['must_not'] = [
382 'terms' => [
383 'comment_post_author_ID' => array_values( (array) $query_vars['post_author__not_in'] ),
384 ],
385 ];
386
387 $use_filters = true;
388 }
389
390 /**
391 * Support `post_id` query var.
392 */
393 if ( ! empty( $query_vars['post_id'] ) ) {
394 $filter['bool']['must'][]['bool']['must'] = [
395 'term' => [
396 'comment_post_ID' => (int) $query_vars['post_id'],
397 ],
398 ];
399
400 $use_filters = true;
401 }
402
403 /**
404 * Support `post__in` query var
405 */
406 if ( ! empty( $query_vars['post__in'] ) ) {
407 $filter['bool']['must'][]['bool']['must'] = [
408 'terms' => [
409 'comment_post_ID' => array_values( (array) $query_vars['post__in'] ),
410 ],
411 ];
412
413 $use_filters = true;
414 }
415
416 /**
417 * Support `post__not_in` query var
418 */
419 if ( ! empty( $query_vars['post__not_in'] ) ) {
420 $filter['bool']['must'][]['bool']['must_not'] = [
421 'terms' => [
422 'comment_post_ID' => array_values( (array) $query_vars['post__not_in'] ),
423 ],
424 ];
425
426 $use_filters = true;
427 }
428
429 /**
430 * Support `post_status` query var
431 */
432 if ( ! empty( $query_vars['post_status'] ) && 'any' !== $query_vars['post_status'] ) {
433 $post_status = (array) ( is_string( $query_vars['post_status'] ) ? explode( ',', $query_vars['post_status'] ) : $query_vars['post_status'] );
434 $post_status = array_map( 'trim', $post_status );
435 $terms_map_name = 'terms';
436
437 if ( count( $post_status ) < 2 ) {
438 $terms_map_name = 'term';
439 $post_status = $post_status[0];
440 }
441
442 $filter['bool']['must'][] = array(
443 $terms_map_name => array(
444 'comment_post_status' => $post_status,
445 ),
446 );
447
448 $use_filters = true;
449 }
450
451 /**
452 * Support `post_type` query var.
453 */
454 if ( ! empty( $query_vars['post_type'] ) ) {
455 $filter['bool']['must'][]['bool']['must'] = [
456 'terms' => [
457 'comment_post_type.raw' => array_values( (array) $query_vars['post_type'] ),
458 ],
459 ];
460
461 $use_filters = true;
462 }
463
464 /**
465 * Support `post_name` query var.
466 */
467 if ( ! empty( $query_vars['post_name'] ) ) {
468 $filter['bool']['must'][]['bool']['must'] = [
469 'term' => [
470 'comment_post_name.raw' => $query_vars['post_name'],
471 ],
472 ];
473
474 $use_filters = true;
475 }
476
477 /**
478 * Support `post_parent` query var.
479 */
480 if ( ! empty( $query_vars['post_parent'] ) ) {
481 $filter['bool']['must'][]['bool']['must'] = [
482 'term' => [
483 'comment_post_parent' => (int) $query_vars['post_parent'],
484 ],
485 ];
486
487 $use_filters = true;
488 }
489
490 /**
491 * Support `search` query_var
492 */
493 if ( ! empty( $query_vars['search'] ) ) {
494
495 $search = ! empty( $query_vars['search'] ) ? $query_vars['search'] : '';
496 $search_fields = [];
497
498 /**
499 * Allow for search field specification
500 */
501 if ( ! empty( $query_vars['search_fields'] ) ) {
502 $search_fields = $query_vars['search_fields'];
503 }
504
505 if ( ! empty( $search_fields ) ) {
506 $prepared_search_fields = [];
507
508 if ( ! empty( $search_fields['meta'] ) ) {
509 $metas = (array) $search_fields['meta'];
510
511 foreach ( $metas as $meta ) {
512 $prepared_search_fields[] = 'meta.' . $meta . '.value';
513 }
514
515 unset( $search_fields['meta'] );
516 }
517
518 $prepared_search_fields = array_merge( $search_fields, $prepared_search_fields );
519 } else {
520 $prepared_search_fields = [
521 'comment_author',
522 'comment_author_email',
523 'comment_author_url',
524 'comment_content',
525 ];
526 }
527
528 /**
529 * Filter default comment search fields
530 *
531 * If you are using the weighting engine, this filter should not be used.
532 * Instead, you should use the ep_weighting_configuration_for_search filter.
533 *
534 * @hook ep_comment_search_fields
535 * @since 3.6.0
536 * @param {array} $search_fields Default search fields
537 * @param {array} $query_vars WP_Comment_Query args
538 * @return {array} New defaults
539 */
540 $prepared_search_fields = apply_filters( 'ep_comment_search_fields', $prepared_search_fields, $query_vars );
541
542 $search_algorithm = $this->get_search_algorithm( $search, $prepared_search_fields, $query_vars );
543 $formatted_args['query'] = $search_algorithm->get_query( 'comment', $search, $prepared_search_fields, $query_vars );
544 } else {
545 $formatted_args['query']['match_all'] = [
546 'boost' => 1,
547 ];
548 }
549
550 /**
551 * Support `status` query var
552 */
553 if ( ! empty( $query_vars['status'] ) && 'all' !== $query_vars['status'] ) {
554 $comment_stati = (array) ( is_string( $query_vars['status'] ) ? explode( ',', $query_vars['status'] ) : $query_vars['status'] );
555 $comment_stati = array_map( 'trim', $comment_stati );
556
557 foreach ( $comment_stati as $key => $status ) {
558 if ( 'hold' === $status ) {
559 $comment_stati[ $key ] = 0;
560 }
561
562 if ( 'approve' === $status ) {
563 $comment_stati[ $key ] = 1;
564 }
565 }
566
567 $terms_map_name = 'terms';
568
569 if ( count( $comment_stati ) < 2 ) {
570 $terms_map_name = 'term';
571 $comment_stati = $comment_stati[0];
572 }
573
574 /**
575 * Support `include_unapproved` query var
576 */
577 if ( ! empty( $query_vars['include_unapproved'] ) ) {
578 $include_unapproved = wp_parse_list( $query_vars['include_unapproved'] );
579 $unapproved_ids = [];
580 $unapproved_emails = [];
581
582 foreach ( $include_unapproved as $unapproved_identifier ) {
583 // Numeric values are assumed to be user ids.
584 if ( is_numeric( $unapproved_identifier ) ) {
585 $unapproved_ids[] = $unapproved_identifier;
586
587 // Otherwise we assume it's an email address.
588 } else {
589 $unapproved_emails[] = $unapproved_identifier;
590 }
591 }
592
593 $filter['bool']['must'][]['bool']['should'] = [
594 [
595 $terms_map_name => [
596 'comment_approved' => $comment_stati,
597 ],
598 ],
599 [
600 'terms' => [
601 'user_id' => array_values( array_map( 'absint', $unapproved_ids ) ),
602 ],
603 ],
604 [
605 'terms' => [
606 'comment_author_email.raw' => array_values( $unapproved_emails ),
607 ],
608 ],
609 ];
610 } else {
611 $filter['bool']['must'][] = [
612 $terms_map_name => [
613 'comment_approved' => $comment_stati,
614 ],
615 ];
616 }
617
618 $use_filters = true;
619 }
620
621 /**
622 * Support `type` query var
623 */
624 if ( ! empty( $query_vars['type'] ) ) {
625 $types = (array) ( is_string( $query_vars['type'] ) ? explode( ',', $query_vars['type'] ) : $query_vars['type'] );
626 $types = array_map( 'trim', $types );
627
628 $terms_map_name = 'terms';
629
630 if ( count( $types ) < 2 ) {
631 $terms_map_name = 'term';
632 $types = $types[0];
633 }
634
635 $filter['bool']['must'][] = [
636 $terms_map_name => [
637 'comment_type.raw' => $types,
638 ],
639 ];
640
641 $use_filters = true;
642 }
643
644 /**
645 * Support `type__in` query var
646 */
647 if ( ! empty( $query_vars['type__in'] ) ) {
648 $filter['bool']['must'][]['bool']['must'] = [
649 'terms' => [
650 'comment_type.raw' => array_values( (array) $query_vars['type__in'] ),
651 ],
652 ];
653
654 $use_filters = true;
655 }
656
657 /**
658 * Support `type__not_in` query var
659 */
660 if ( ! empty( $query_vars['type__not_in'] ) ) {
661 $filter['bool']['must'][]['bool']['must_not'] = [
662 'terms' => [
663 'comment_type.raw' => array_values( (array) $query_vars['type__not_in'] ),
664 ],
665 ];
666
667 $use_filters = true;
668 }
669
670 if ( $use_filters ) {
671 $formatted_args['post_filter'] = $filter;
672 }
673
674 /**
675 * Filter formatted Elasticsearch query (entire query)
676 *
677 * @hook ep_comment_formatted_args
678 * @since 3.6.0
679 * @param {array} $formatted_args Formatted Elasticsearch query
680 * @param {array} $query_vars WP_Comment_Query args
681 * @return {array} New query
682 */
683 return apply_filters( 'ep_comment_formatted_args', $formatted_args, $query_vars );
684 }
685
686 /**
687 * Generate the mapping array
688 *
689 * @since 4.1.0
690 * @return array
691 */
692 public function generate_mapping() {
693 $es_version = Elasticsearch::factory()->get_elasticsearch_version();
694
695 if ( empty( $es_version ) ) {
696 /**
697 * Filter fallback Elasticsearch version
698 *
699 * @hook ep_fallback_elasticsearch_version
700 * @param {string} $version Fall back Elasticsearch version
701 * @return {string} New version
702 */
703 $es_version = apply_filters( 'ep_fallback_elasticsearch_version', '2.0' );
704 }
705
706 $es_version = (string) $es_version;
707
708 $mapping_file = '7-0.php';
709
710 if ( version_compare( $es_version, '7.0', '<' ) ) {
711 $mapping_file = 'initial.php';
712 }
713
714 /**
715 * Filter comment indexable mapping file
716 *
717 * @hook ep_comment_mapping_file
718 * @since 3.6.0
719 * @param {string} $file Path to file
720 * @return {string} New file path
721 */
722 $mapping = require apply_filters( 'ep_comment_mapping_file', __DIR__ . '/../../../mappings/comment/' . $mapping_file );
723
724 /**
725 * Filter comment indexable mapping
726 *
727 * @hook ep_comment_mapping
728 * @since 3.6.0
729 * @param {array} $mapping Mapping
730 * @return {array} New mapping
731 */
732 $mapping = apply_filters( 'ep_comment_mapping', $mapping );
733
734 return $mapping;
735 }
736
737 /**
738 * Returns indexable comment types
739 *
740 * @since 3.6.0
741 * @return array
742 */
743 public function get_indexable_comment_types() {
744 $comment_types = [ 'comment' ];
745
746 if ( Features::factory()->registered_features['woocommerce']->is_active() ) {
747 $comment_types[] = 'review';
748 }
749
750 /**
751 * Filter indexable comment types
752 *
753 * @hook ep_indexable_comment_types
754 * @since 3.6.0
755 * @param {array} $comment_types Indexable comment types
756 * @return {array} comment types
757 */
758 return apply_filters( 'ep_indexable_comment_types', $comment_types );
759 }
760
761 /**
762 * Returns indexable comment status
763 *
764 * @since 3.6.0
765 * @return array
766 */
767 public function get_indexable_comment_status() {
768 $comment_status = [ '1' ];
769
770 /**
771 * Filter indexable comment status
772 *
773 * @hook ep_indexable_comment_status
774 * @since 3.6.0
775 * @param {array} $comment_status Indexable comment status
776 * @return {array} comment status
777 */
778 return apply_filters( 'ep_indexable_comment_status', $comment_status );
779 }
780
781 /**
782 * Query DB for comments
783 *
784 * @param array $args Query arguments
785 * @since 3.6.0
786 * @return array
787 */
788 public function query_db( $args ) {
789 $defaults = [
790 'type' => $this->get_indexable_comment_types(),
791 'status' => $this->get_indexable_comment_status(),
792 'post_type' => Indexables::factory()->get( 'post' )->get_indexable_post_types(),
793 'post_status' => Indexables::factory()->get( 'post' )->get_indexable_post_status(),
794 'number' => $this->get_bulk_items_per_page(),
795 'offset' => 0,
796 'orderby' => 'comment_ID',
797 'order' => 'desc',
798 'ep_indexing_advanced_pagination' => true,
799 'no_found_rows' => false,
800 ];
801
802 if ( isset( $args['per_page'] ) ) {
803 $args['number'] = $args['per_page'];
804 }
805
806 if ( isset( $args['include'] ) ) {
807 $args['comment__in'] = $args['include'];
808 }
809
810 if ( isset( $args['exclude'] ) ) {
811 $args['comment__not_in'] = $args['exclude'];
812 }
813
814 /**
815 * Filter database arguments for comment query
816 *
817 * @hook ep_comment_query_db_args
818 * @param {array} $args Query arguments based to WP_Comment_Query
819 * @since 3.6.0
820 * @return {array} New arguments
821 */
822 $args = apply_filters( 'ep_comment_query_db_args', wp_parse_args( $args, $defaults ) );
823
824 $all_query_args = $args;
825
826 unset( $all_query_args['number'] );
827 unset( $all_query_args['offset'] );
828 $all_query_args['count'] = true;
829
830 if ( isset( $args['comment__in'] ) || 0 < $args['offset'] ) {
831 // Disable advanced pagination. Not useful if only indexing specific IDs.
832 $args['ep_indexing_advanced_pagination'] = false;
833 }
834
835 // Explicitly set the orderby to ID to prevent accidental modifications by other code.
836 add_filter( 'comments_clauses', [ $this, 'set_orderby' ], 9999, 2 );
837
838 // Enforce the following query args during advanced pagination to ensure things work correctly.
839 if ( $args['ep_indexing_advanced_pagination'] ) {
840 $args = array_merge(
841 $args,
842 [
843 'suppress_filters' => false,
844 'orderby' => 'comment_ID',
845 'order' => 'desc',
846 'paged' => 1,
847 'offset' => 0,
848 ]
849 );
850
851 // It's important to pass a custom cache domain. By default, WordPress caches results based on the default query arguments and doesn't account for custom arguments. @see \WP_Comment_Query::get_comments()
852 $cache_key = md5( get_current_blog_id() . wp_json_encode( $args ) );
853 $args['cache_domain'] = 'elasticpress-comment-indexable-' . $cache_key;
854
855 add_filter( 'comments_clauses', array( $this, 'bulk_indexing_filter_comments_where' ), 9999, 2 );
856
857 $query = new WP_Comment_Query( $args );
858 $total_objects = $this->get_total_objects_for_query( $args );
859 remove_filter( 'comments_clauses', array( $this, 'bulk_indexing_filter_comments_where' ), 9999, 2 );
860 } else {
861 $query = new WP_Comment_Query( $args );
862 $total_objects = $query->found_comments;
863 }
864
865 remove_filter( 'comments_clauses', [ $this, 'set_orderby' ], 9999, 2 );
866
867 if ( is_array( $query->comments ) ) {
868 array_walk( $query->comments, [ $this, 'remap_comments' ] );
869 }
870
871 return [
872 'objects' => $query->comments,
873 'total_objects' => $total_objects,
874 ];
875 }
876
877 /**
878 * Filters the WHERE clause of the SQL query used for bulk indexing comments by modifying it to include a range
879 * of comment IDs based on advanced pagination parameters.
880 *
881 * @param array $clauses Associative array of the clauses for the query.
882 * @param \WP_Comment_Query $query The current WP_Comment_Query instance.
883 *
884 * @return array Modified SQL query clauses.
885 */
886 public function bulk_indexing_filter_comments_where( $clauses, $query ) {
887 global $wpdb;
888
889 $using_advanced_pagination = $this->get_query_var( $query, 'ep_indexing_advanced_pagination', false );
890
891 if ( $using_advanced_pagination ) {
892 $requested_upper_limit_id = $this->get_query_var( $query, 'ep_indexing_upper_limit_object_id', PHP_INT_MAX );
893 $requested_lower_limit_object_id = $this->get_query_var( $query, 'ep_indexing_lower_limit_object_id', 0 );
894 $last_processed_id = $this->get_query_var( $query, 'ep_indexing_last_processed_object_id', null );
895
896 // On the first loopthrough we begin with the requested upper limit ID. Afterwards, use the last processed ID to paginate.
897 $upper_limit_range_object_id = $requested_upper_limit_id;
898 if ( is_numeric( $last_processed_id ) ) {
899 $upper_limit_range_object_id = $last_processed_id - 1;
900 }
901
902 // Sanitize. Abort if unexpected data at this point.
903 if ( ! is_numeric( $upper_limit_range_object_id ) || ! is_numeric( $requested_lower_limit_object_id ) ) {
904 return $clauses;
905 }
906
907 $range = [
908 'upper_limit' => "{$wpdb->comments}.comment_ID <= {$upper_limit_range_object_id}",
909 'lower_limit' => "{$wpdb->comments}.comment_ID >= {$requested_lower_limit_object_id}",
910 ];
911
912 // Skip the end range if it's unnecessary.
913 $skip_ending_range = 0 === $requested_lower_limit_object_id;
914 $where = $clauses['where'];
915 $where = $skip_ending_range ? " {$range['upper_limit']} AND {$where}" : " {$range['upper_limit']} AND {$range['lower_limit']} AND {$where}";
916
917 $clauses['where'] = $where;
918 }
919
920 return $clauses;
921 }
922
923 /**
924 * Get the total number of comments for a given query.
925 *
926 * @param array $query_args The query args.
927 * @return int The query result's found_comments.
928 */
929 protected function get_total_objects_for_query( $query_args ) {
930 $normalized_query_args = array_merge(
931 $query_args,
932 [
933 'offset' => 0,
934 'paged' => 1,
935 'posts_per_page' => 1,
936 'no_found_rows' => false,
937 'ep_indexing_last_processed_object_id' => null,
938 ]
939 );
940
941 $cache_key = md5( get_current_blog_id() . wp_json_encode( $normalized_query_args ) );
942
943 $normalized_query_args['cache_domain'] = 'elasticpress-comment-indexable-' . $cache_key;
944
945 return ( new WP_Comment_Query( $normalized_query_args ) )->found_comments;
946 }
947
948 /**
949 * Prepare a comment document for indexing
950 *
951 * @param int $comment_id Comment ID
952 * @since 3.6.0
953 * @return bool|array
954 */
955 public function prepare_document( $comment_id ) {
956 $comment = get_comment( $comment_id );
957
958 if ( ! $comment || ! is_a( $comment, 'WP_Comment' ) ) {
959 return false;
960 }
961
962 $comment_post = get_post( $comment->comment_post_ID );
963
964 $comment_args = [
965 'comment_ID' => $comment->comment_ID,
966 'ID' => $comment->comment_ID,
967 'comment_post_ID' => $comment->comment_post_ID,
968 'comment_post_author_ID' => $comment_post->post_author,
969 'comment_post_status' => $comment_post->post_status,
970 'comment_post_type' => $comment_post->post_type,
971 'comment_post_name' => $comment_post->post_name,
972 'comment_post_parent' => $comment_post->post_parent,
973 'comment_author' => $comment->comment_author,
974 'comment_author_email' => $comment->comment_author_email,
975 'comment_author_url' => $comment->comment_author_url,
976 'comment_author_IP' => $comment->comment_author_IP,
977 'comment_date' => $comment->comment_date,
978 'comment_date_gmt' => $comment->comment_date_gmt,
979 'comment_content' => $comment->comment_content,
980 'comment_karma' => $comment->comment_karma,
981 'comment_approved' => $comment->comment_approved,
982 'comment_agent' => $comment->comment_agent,
983 'comment_type' => $comment->comment_type ? $comment->comment_type : 'comment',
984 'comment_parent' => $comment->comment_parent,
985 'user_id' => $comment->user_id,
986 'meta' => $this->prepare_meta_types( $this->prepare_meta( $comment->comment_ID ) ),
987 ];
988
989 /**
990 * Filter sync arguments for a comment.
991 *
992 * @hook ep_comment_sync_args
993 * @param {array} $comment_args Comment arguments
994 * @param {int} $comment_id Comment ID
995 * @return {array} New arguments
996 */
997 $comment_args = apply_filters( 'ep_comment_sync_args', $comment_args, $comment_id );
998
999 return $comment_args;
1000 }
1001
1002 /**
1003 * Rebuild our comment objects to match the fields we need.
1004 *
1005 * In particular, result of WP_Comment_Query does not
1006 * include an "id" field, which our index command
1007 * expects.
1008 *
1009 * @param object $value Comment object
1010 * @since 3.6.0
1011 * @return void Returns by reference
1012 */
1013 public function remap_comments( &$value ) {
1014 $value = (object) [
1015 'ID' => $value->comment_ID,
1016 'comment_ID' => $value->comment_ID,
1017 'comment_post_ID' => $value->comment_post_ID,
1018 'comment_author' => $value->comment_author,
1019 'comment_author_email' => $value->comment_author_email,
1020 'comment_author_url' => $value->comment_author_url,
1021 'comment_author_IP' => $value->comment_author_IP,
1022 'comment_date' => $value->comment_date,
1023 'comment_date_gmt' => $value->comment_date_gmt,
1024 'comment_content' => $value->comment_content,
1025 'comment_karma' => $value->comment_karma,
1026 'comment_approved' => $value->comment_approved,
1027 'comment_agent' => $value->comment_agent,
1028 'comment_type' => $value->comment_type,
1029 'comment_parent' => $value->comment_parent,
1030 'user_id' => $value->user_id,
1031 ];
1032 }
1033
1034 /**
1035 * Prepare meta to send to ES
1036 *
1037 * @param int $comment_id Comment ID
1038 * @since 3.6.0
1039 * @return array
1040 */
1041 public function prepare_meta( $comment_id ) {
1042 $meta = (array) get_comment_meta( $comment_id );
1043
1044 if ( empty( $meta ) ) {
1045 return [];
1046 }
1047
1048 $prepared_meta = [];
1049
1050 /**
1051 * Filter index-able private meta
1052 *
1053 * Allows for specifying private meta keys that may be indexed in the same manner as public meta keys.
1054 *
1055 * @since 3.6.0
1056 *
1057 * @param array Array of index-able private meta keys.
1058 * @param int $comment_id Comment ID.
1059 */
1060 $allowed_protected_keys = apply_filters(
1061 'ep_prepare_comment_meta_allowed_protected_keys',
1062 [],
1063 $comment_id
1064 );
1065
1066 /**
1067 * Filter non-indexed public meta
1068 *
1069 * Allows for specifying public meta keys that should be excluded from the ElasticPress index.
1070 *
1071 * @since 3.6.0
1072 *
1073 * @param array Array of public meta keys to exclude from index.
1074 * @param int $comment_id Comment ID.
1075 */
1076 $excluded_public_keys = apply_filters(
1077 'ep_prepare_comment_meta_excluded_public_keys',
1078 [],
1079 $comment_id
1080 );
1081
1082 foreach ( $meta as $key => $value ) {
1083
1084 $allow_index = false;
1085
1086 if ( is_protected_meta( $key ) ) {
1087
1088 if ( true === $allowed_protected_keys || in_array( $key, $allowed_protected_keys, true ) ) {
1089 $allow_index = true;
1090 }
1091 } elseif ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) {
1092
1093 $allow_index = true;
1094 }
1095
1096 /**
1097 * Filter force allow a meta key
1098 *
1099 * @hook ep_prepare_comment_meta_allowed_key
1100 * @since 3.6.0
1101 * @param {bool} $allowed True to allow the key
1102 * @param {string} $key Meta key
1103 * @param {int} $comment_id Comment ID
1104 * @return {bool} New allowed value
1105 */
1106 if ( true === $allow_index || apply_filters( 'ep_prepare_comment_meta_allowed_key', false, $key, $comment_id ) ) {
1107 $prepared_meta[ $key ] = maybe_unserialize( $value );
1108 }
1109 }
1110
1111 return $prepared_meta;
1112 }
1113
1114 /**
1115 * Parse an 'order' query variable and cast it to asc or desc as necessary.
1116 *
1117 * @access protected
1118 *
1119 * @param string $order The 'order' query variable.
1120 * @since 3.6.0
1121 * @return string The sanitized 'order' query variable.
1122 */
1123 protected function parse_order( $order ) {
1124 if ( ! is_string( $order ) || empty( $order ) ) {
1125 return 'desc';
1126 }
1127
1128 if ( 'ASC' === strtoupper( $order ) ) {
1129 return 'asc';
1130 } else {
1131 return 'desc';
1132 }
1133 }
1134
1135 /**
1136 * Convert the alias to a properly-prefixed sort value.
1137 *
1138 * @access protected
1139 *
1140 * @param string $orderby Alias or path for the field to order by.
1141 * @param string $order Order direction
1142 * @param array $args Query args
1143 * @since 3.6.0
1144 * @return array
1145 */
1146 protected function parse_orderby( $orderby, $order, $args ) {
1147 $sort = [];
1148
1149 if ( empty( $orderby ) ) {
1150 return $sort;
1151 }
1152
1153 $from_to = [
1154 'comment_agent' => 'comment_agent.raw',
1155 'comment_approved' => 'comment_approved.raw',
1156 'comment_author' => 'comment_author.raw',
1157 'comment_author_email' => 'comment_author_email.raw',
1158 'comment_author_IP' => 'comment_author_IP.raw',
1159 'comment_author_url' => 'comment_author_url.raw',
1160 'comment_content' => 'comment_content.raw',
1161 'comment_type' => 'comment_type.raw',
1162 'comment_post_type' => 'comment_post_type.raw',
1163 ];
1164
1165 if ( in_array( $orderby, [ 'meta_value', 'meta_value_num' ], true ) ) {
1166 if ( empty( $args['meta_key'] ) ) {
1167 return $sort;
1168 } else {
1169 $from_to['meta_value'] = 'meta.' . $args['meta_key'] . '.raw';
1170 $from_to['meta_value_num'] = 'meta.' . $args['meta_key'] . '.long';
1171 }
1172 }
1173
1174 /**
1175 * If `orderby` is 'none', WordPress will let the database decide on what should be used to order.
1176 * It will use the primary key ASC.
1177 */
1178 if ( 'none' === $orderby ) {
1179 $orderby = 'ID';
1180 $order = 'asc';
1181 }
1182
1183 $orderby = $from_to[ $orderby ] ?? $orderby;
1184
1185 $sort[] = array(
1186 $orderby => array(
1187 'order' => $order,
1188 ),
1189 );
1190
1191 return $sort;
1192 }
1193
1194 /**
1195 * Retrieve a specific query variable from the query object.
1196 *
1197 * @param \WP_Comment_Query $query The query object.
1198 * @param string $query_var The name of the query variable to retrieve.
1199 * @param string $default_value The default value to return if the query variable is not set. Default is an empty string.
1200 *
1201 * @return mixed The value of the query variable if set, otherwise the default value.
1202 */
1203 public function get_query_var( $query, $query_var, $default_value = '' ) {
1204 return $query->query_vars[ $query_var ] ?? $default_value;
1205 }
1206
1207 /**
1208 * Sets the ORDER BY clause for comment queries to order comments by their ID.
1209 *
1210 * @param array $clauses The SQL clauses array to modify.
1211 * @return array The modified SQL clauses array with the ORDER BY clause set.
1212 *
1213 * @since 5.2.0
1214 */
1215 public function set_orderby( $clauses ) {
1216 global $wpdb;
1217
1218 $clauses['orderby'] = "{$wpdb->comments}.comment_ID DESC";
1219 return $clauses;
1220 }
1221 }
1222