PluginProbe
ActivityPub / 5.1.0
ActivityPub v5.1.0
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / class-comment.php

class-comment.php in ActivityPub 5.1.0, at includes/class-comment.php

789 lines 22.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ActivityPub Comment Class
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Collection\Actors;
11 use WP_Comment_Query;
12
13 /**
14 * ActivityPub Comment Class.
15 *
16 * This class is a helper/utils class that provides a collection of static
17 * methods that are used to handle comments.
18 */
19 class Comment {
20 /**
21 * Initialize the class, registering WordPress hooks.
22 */
23 public static function init() {
24 self::register_comment_types();
25
26 \add_filter( 'comment_reply_link', array( self::class, 'comment_reply_link' ), 10, 3 );
27 \add_filter( 'comment_class', array( self::class, 'comment_class' ), 10, 3 );
28 \add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 3 );
29 \add_action( 'wp_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
30 \add_action( 'pre_get_comments', array( static::class, 'comment_query' ) );
31 \add_filter( 'pre_comment_approved', array( static::class, 'pre_comment_approved' ), 10, 2 );
32 \add_filter( 'get_avatar_comment_types', array( static::class, 'get_avatar_comment_types' ), 99 );
33 \add_filter( 'pre_wp_update_comment_count_now', array( static::class, 'pre_wp_update_comment_count_now' ), 10, 3 );
34 }
35
36 /**
37 * Filter the comment reply link.
38 *
39 * We don't want to show the comment reply link for federated comments
40 * if the user is disabled for federation.
41 *
42 * @param string $link The HTML markup for the comment reply link.
43 * @param array $args An array of arguments overriding the defaults.
44 * @param \WP_Comment $comment The object of the comment being replied.
45 *
46 * @return string The filtered HTML markup for the comment reply link.
47 */
48 public static function comment_reply_link( $link, $args, $comment ) {
49 if ( self::are_comments_allowed( $comment ) ) {
50 $user_id = get_current_user_id();
51 if ( $user_id && self::was_received( $comment ) && \user_can( $user_id, 'activitypub' ) ) {
52 return self::create_fediverse_reply_link( $link, $args );
53 }
54
55 return $link;
56 }
57
58 $attrs = array(
59 'selectedComment' => self::generate_id( $comment ),
60 'commentId' => $comment->comment_ID,
61 );
62
63 $div = sprintf(
64 '<div class="activitypub-remote-reply" data-attrs="%s"></div>',
65 esc_attr( wp_json_encode( $attrs ) )
66 );
67
68 /**
69 * Filters the HTML markup for the ActivityPub remote comment reply container.
70 *
71 * @param string $div The HTML markup for the remote reply container. Default is a div
72 * with class 'activitypub-remote-reply' and data attributes for
73 * the selected comment ID and internal comment ID.
74 */
75 return apply_filters( 'activitypub_comment_reply_link', $div );
76 }
77
78 /**
79 * Create a link to reply to a federated comment.
80 *
81 * This function adds a title attribute to the reply link to inform the user
82 * that the comment was received from the fediverse and the reply will be sent
83 * to the original author.
84 *
85 * @param string $link The HTML markup for the comment reply link.
86 * @param array $args The args provided by the `comment_reply_link` filter.
87 *
88 * @return string The modified HTML markup for the comment reply link.
89 */
90 private static function create_fediverse_reply_link( $link, $args ) {
91 $str_to_replace = sprintf( '>%s<', $args['reply_text'] );
92 $replace_with = sprintf(
93 ' title="%s">%s<',
94 esc_attr__( 'This comment was received from the fediverse and your reply will be sent to the original author', 'activitypub' ),
95 esc_html__( 'Reply with federation', 'activitypub' )
96 );
97 return str_replace( $str_to_replace, $replace_with, $link );
98 }
99
100 /**
101 * Check if it is allowed to comment to a comment.
102 *
103 * Checks if the comment is local only or if the user can comment federated comments.
104 *
105 * @param mixed $comment Comment object or ID.
106 *
107 * @return boolean True if the user can comment, false otherwise.
108 */
109 public static function are_comments_allowed( $comment ) {
110 $comment = \get_comment( $comment );
111
112 if ( ! self::was_received( $comment ) ) {
113 return true;
114 }
115
116 $current_user = get_current_user_id();
117
118 if ( ! $current_user ) {
119 return false;
120 }
121
122 if ( is_single_user() && \user_can( $current_user, 'publish_posts' ) ) {
123 // On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user.
124 $current_user = Actors::BLOG_USER_ID;
125 }
126
127 $is_user_disabled = is_user_disabled( $current_user );
128
129 if ( $is_user_disabled ) {
130 return false;
131 }
132
133 return true;
134 }
135
136 /**
137 * Check if a comment is federated.
138 *
139 * We consider a comment federated if comment was received via ActivityPub.
140 *
141 * Use this function to check if it is comment that was received via ActivityPub.
142 *
143 * @param mixed $comment Comment object or ID.
144 *
145 * @return boolean True if the comment is federated, false otherwise.
146 */
147 public static function was_received( $comment ) {
148 $comment = \get_comment( $comment );
149
150 if ( ! $comment ) {
151 return false;
152 }
153
154 $protocol = \get_comment_meta( $comment->comment_ID, 'protocol', true );
155
156 if ( 'activitypub' === $protocol ) {
157 return true;
158 }
159
160 return false;
161 }
162
163 /**
164 * Check if a comment was federated.
165 *
166 * This function checks if a comment was federated via ActivityPub.
167 *
168 * @param mixed $comment Comment object or ID.
169 *
170 * @return boolean True if the comment was federated, false otherwise.
171 */
172 public static function was_sent( $comment ) {
173 $comment = \get_comment( $comment );
174
175 if ( ! $comment ) {
176 return false;
177 }
178
179 $status = \get_comment_meta( $comment->comment_ID, 'activitypub_status', true );
180
181 if ( $status ) {
182 return true;
183 }
184
185 return false;
186 }
187
188 /**
189 * Check if a comment is local only.
190 *
191 * This function checks if a comment is local only and was not sent or received via ActivityPub.
192 *
193 * @param mixed $comment Comment object or ID.
194 *
195 * @return boolean True if the comment is local only, false otherwise.
196 */
197 public static function is_local( $comment ) {
198 if ( self::was_sent( $comment ) || self::was_received( $comment ) ) {
199 return false;
200 }
201
202 return true;
203 }
204
205 /**
206 * Check if a comment should be federated.
207 *
208 * We consider a comment should be federated if it is authored by a user that is
209 * not disabled for federation and if it is a reply directly to the post or to a
210 * federated comment.
211 *
212 * Use this function to check if a comment should be federated.
213 *
214 * @param mixed $comment Comment object or ID.
215 *
216 * @return boolean True if the comment should be federated, false otherwise.
217 */
218 public static function should_be_federated( $comment ) {
219 // We should not federate federated comments.
220 if ( self::was_received( $comment ) ) {
221 return false;
222 }
223
224 $comment = \get_comment( $comment );
225 $user_id = $comment->user_id;
226
227 // Comments without user can't be federated.
228 if ( ! $user_id ) {
229 return false;
230 }
231
232 if ( is_single_user() && \user_can( $user_id, 'activitypub' ) ) {
233 // On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user.
234 $user_id = Actors::BLOG_USER_ID;
235 }
236
237 $is_user_disabled = is_user_disabled( $user_id );
238
239 // User is disabled for federation.
240 if ( $is_user_disabled ) {
241 return false;
242 }
243
244 // It is a comment to the post and can be federated.
245 if ( empty( $comment->comment_parent ) ) {
246 return true;
247 }
248
249 // Check if parent comment is federated.
250 $parent_comment = \get_comment( $comment->comment_parent );
251
252 return ! self::is_local( $parent_comment );
253 }
254
255 /**
256 * Examine a comment ID and look up an existing comment it represents.
257 *
258 * @param string $id ActivityPub object ID (usually a URL) to check.
259 *
260 * @return \WP_Comment|false Comment object, or false on failure.
261 */
262 public static function object_id_to_comment( $id ) {
263 $comment_query = new WP_Comment_Query(
264 array(
265 'meta_key' => 'source_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
266 'meta_value' => $id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
267 )
268 );
269
270 if ( ! $comment_query->comments ) {
271 return false;
272 }
273
274 if ( count( $comment_query->comments ) > 1 ) {
275 return false;
276 }
277
278 return $comment_query->comments[0];
279 }
280
281 /**
282 * Verify if URL is a local comment, or if it is a previously received
283 * remote comment (For threading comments locally).
284 *
285 * @param string $url The URL to check.
286 *
287 * @return string|null Comment ID or null if not found.
288 */
289 public static function url_to_commentid( $url ) {
290 if ( ! $url || ! filter_var( $url, \FILTER_VALIDATE_URL ) ) {
291 return null;
292 }
293
294 // Check for local comment.
295 if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) === \wp_parse_url( $url, \PHP_URL_HOST ) ) {
296 $query = \wp_parse_url( $url, \PHP_URL_QUERY );
297
298 if ( $query ) {
299 parse_str( $query, $params );
300
301 if ( ! empty( $params['c'] ) ) {
302 $comment = \get_comment( $params['c'] );
303
304 if ( $comment ) {
305 return $comment->comment_ID;
306 }
307 }
308 }
309 }
310
311 $args = array(
312 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
313 'meta_query' => array(
314 'relation' => 'OR',
315 array(
316 'key' => 'source_url',
317 'value' => $url,
318 ),
319 array(
320 'key' => 'source_id',
321 'value' => $url,
322 ),
323 ),
324 );
325
326 $query = new WP_Comment_Query();
327 $comments = $query->query( $args );
328
329 if ( $comments && is_array( $comments ) ) {
330 return $comments[0]->comment_ID;
331 }
332
333 return null;
334 }
335
336 /**
337 * Filters the CSS classes to add an ActivityPub class.
338 *
339 * @param string[] $classes An array of comment classes.
340 * @param string[] $css_class An array of additional classes added to the list.
341 * @param string $comment_id The comment ID as a numeric string.
342 *
343 * @return string[] An array of classes.
344 */
345 public static function comment_class( $classes, $css_class, $comment_id ) {
346 // Check if ActivityPub comment.
347 if ( 'activitypub' === get_comment_meta( $comment_id, 'protocol', true ) ) {
348 $classes[] = 'activitypub-comment';
349 }
350
351 return $classes;
352 }
353
354 /**
355 * Gets the public comment id via the WordPress comments meta.
356 *
357 * @param int $wp_comment_id The internal WordPress comment ID.
358 * @param bool $fallback Whether the code should fall back to `source_url` if `source_id` is not set.
359 *
360 * @return string|null The ActivityPub id/url of the comment.
361 */
362 public static function get_source_id( $wp_comment_id, $fallback = true ) {
363 $comment_meta = \get_comment_meta( $wp_comment_id );
364
365 if ( ! empty( $comment_meta['source_id'][0] ) ) {
366 return $comment_meta['source_id'][0];
367 } elseif ( ! empty( $comment_meta['source_url'][0] ) && $fallback ) {
368 return $comment_meta['source_url'][0];
369 }
370
371 return null;
372 }
373
374 /**
375 * Gets the public comment url via the WordPress comments meta.
376 *
377 * @param int $wp_comment_id The internal WordPress comment ID.
378 * @param bool $fallback Whether the code should fall back to `source_id` if `source_url` is not set.
379 *
380 * @return string|null The ActivityPub id/url of the comment.
381 */
382 public static function get_source_url( $wp_comment_id, $fallback = true ) {
383 $comment_meta = \get_comment_meta( $wp_comment_id );
384
385 if ( ! empty( $comment_meta['source_url'][0] ) ) {
386 return $comment_meta['source_url'][0];
387 } elseif ( ! empty( $comment_meta['source_id'][0] ) && $fallback ) {
388 return $comment_meta['source_id'][0];
389 }
390
391 return null;
392 }
393
394 /**
395 * Link remote comments to source url.
396 *
397 * @param string $comment_link The comment link.
398 * @param object|\WP_Comment $comment The comment object.
399 *
400 * @return string $url
401 */
402 public static function remote_comment_link( $comment_link, $comment ) {
403 if ( ! $comment || is_admin() ) {
404 return $comment_link;
405 }
406
407 $public_comment_link = self::get_source_url( $comment->comment_ID );
408
409 return $public_comment_link ?? $comment_link;
410 }
411
412
413 /**
414 * Generates an ActivityPub URI for a comment
415 *
416 * @param \WP_Comment|int $comment A comment object or comment ID.
417 *
418 * @return string ActivityPub URI for comment
419 */
420 public static function generate_id( $comment ) {
421 $comment = \get_comment( $comment );
422
423 // Show external comment ID if it exists.
424 $public_comment_link = self::get_source_id( $comment->comment_ID );
425
426 if ( $public_comment_link ) {
427 return $public_comment_link;
428 }
429
430 // Generate URI based on comment ID.
431 return \add_query_arg( 'c', $comment->comment_ID, \trailingslashit( \home_url() ) );
432 }
433
434 /**
435 * Check if a post has remote comments
436 *
437 * @param int $post_id The post ID.
438 *
439 * @return bool True if the post has remote comments, false otherwise.
440 */
441 private static function post_has_remote_comments( $post_id ) {
442 $comments = \get_comments(
443 array(
444 'post_id' => $post_id,
445 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
446 'meta_query' => array(
447 'relation' => 'AND',
448 array(
449 'key' => 'protocol',
450 'value' => 'activitypub',
451 'compare' => '=',
452 ),
453 array(
454 'key' => 'source_id',
455 'compare' => 'EXISTS',
456 ),
457 ),
458 )
459 );
460
461 return ! empty( $comments );
462 }
463
464 /**
465 * Enqueue scripts for remote comments
466 */
467 public static function enqueue_scripts() {
468 if ( ! \is_singular() || \is_user_logged_in() ) {
469 // Only on single pages, only for logged-out users.
470 return;
471 }
472
473 if ( ! \post_type_supports( \get_post_type(), 'activitypub' ) ) {
474 // Post type does not support ActivityPub.
475 return;
476 }
477
478 if ( ! \comments_open() || ! \get_comments_number() ) {
479 // No comments, no need to load the script.
480 return;
481 }
482
483 if ( ! self::post_has_remote_comments( \get_the_ID() ) ) {
484 // No remote comments, no need to load the script.
485 return;
486 }
487
488 $handle = 'activitypub-remote-reply';
489 $data = array(
490 'namespace' => ACTIVITYPUB_REST_NAMESPACE,
491 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg',
492 );
493 $js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
494 $asset_file = ACTIVITYPUB_PLUGIN_DIR . 'build/remote-reply/index.asset.php';
495
496 if ( \file_exists( $asset_file ) ) {
497 $assets = require_once $asset_file;
498
499 \wp_enqueue_script(
500 $handle,
501 \plugins_url( 'build/remote-reply/index.js', __DIR__ ),
502 $assets['dependencies'],
503 $assets['version'],
504 true
505 );
506 \wp_add_inline_script( $handle, $js, 'before' );
507
508 \wp_enqueue_style(
509 $handle,
510 \plugins_url( 'build/remote-reply/style-index.css', __DIR__ ),
511 array( 'wp-components' ),
512 $assets['version']
513 );
514 }
515 }
516
517 /**
518 * Get the comment type by activity type.
519 *
520 * @param string $activity_type The activity type.
521 *
522 * @return array|null The comment type.
523 */
524 public static function get_comment_type_by_activity_type( $activity_type ) {
525 $activity_type = \strtolower( $activity_type );
526 $activity_type = \sanitize_key( $activity_type );
527 $comment_types = self::get_comment_types();
528
529 foreach ( $comment_types as $comment_type ) {
530 if ( in_array( $activity_type, $comment_type['activity_types'], true ) ) {
531 return $comment_type;
532 }
533 }
534
535 return null;
536 }
537
538 /**
539 * Return the registered custom comment types.
540 *
541 * @return array The registered custom comment types
542 */
543 public static function get_comment_types() {
544 global $activitypub_comment_types;
545
546 return $activitypub_comment_types;
547 }
548
549 /**
550 * Is this a registered comment type.
551 *
552 * @param string $slug The slug of the type.
553 *
554 * @return boolean True if registered.
555 */
556 public static function is_registered_comment_type( $slug ) {
557 $slug = \strtolower( $slug );
558 $slug = \sanitize_key( $slug );
559
560 $comment_types = self::get_comment_types();
561
562 return isset( $comment_types[ $slug ] );
563 }
564
565 /**
566 * Return the registered custom comment type slugs.
567 *
568 * @return array The registered custom comment type slugs.
569 */
570 public static function get_comment_type_slugs() {
571 return array_keys( self::get_comment_types() );
572 }
573
574 /**
575 * Return the registered custom comment type slugs.
576 *
577 * @deprecated 4.5.0 Use get_comment_type_slugs instead.
578 *
579 * @return array The registered custom comment type slugs.
580 */
581 public static function get_comment_type_names() {
582 _deprecated_function( __METHOD__, '4.5.0', 'get_comment_type_slugs' );
583
584 return self::get_comment_type_slugs();
585 }
586
587 /**
588 * Get the custom comment type.
589 *
590 * Check if the type is registered, if not, check if it is a custom type.
591 *
592 * It looks for the array key in the registered types and returns the array.
593 * If it is not found, it looks for the type in the custom types and returns the array.
594 *
595 * @param string $type The comment type.
596 *
597 * @return array The comment type.
598 */
599 public static function get_comment_type( $type ) {
600 $type = strtolower( $type );
601 $type = sanitize_key( $type );
602
603 $comment_types = self::get_comment_types();
604 $type_array = array();
605
606 // Check array keys.
607 if ( in_array( $type, array_keys( $comment_types ), true ) ) {
608 $type_array = $comment_types[ $type ];
609 }
610
611 /**
612 * Filter the comment type.
613 *
614 * @param array $type_array The comment type.
615 */
616 return apply_filters( "activitypub_comment_type_{$type}", $type_array );
617 }
618
619 /**
620 * Get a comment type attribute.
621 *
622 * @param string $type The comment type.
623 * @param string $attr The attribute to get.
624 *
625 * @return mixed The value of the attribute.
626 */
627 public static function get_comment_type_attr( $type, $attr ) {
628 $type_array = self::get_comment_type( $type );
629
630 if ( $type_array && isset( $type_array[ $attr ] ) ) {
631 $value = $type_array[ $attr ];
632 } else {
633 $value = '';
634 }
635
636 /**
637 * Filter the comment type attribute.
638 *
639 * @param mixed $value The value of the attribute.
640 * @param string $type The comment type.
641 */
642 return apply_filters( "activitypub_comment_type_{$attr}", $value, $type );
643 }
644
645 /**
646 * Register the comment types used by the ActivityPub plugin.
647 */
648 public static function register_comment_types() {
649 register_comment_type(
650 'repost',
651 array(
652 'label' => __( 'Reposts', 'activitypub' ),
653 'singular' => __( 'Repost', 'activitypub' ),
654 'description' => __( 'A repost on the indieweb is a post that is purely a 100% re-publication of another (typically someone else\'s) post.', 'activitypub' ),
655 'icon' => '♻️',
656 'class' => 'p-repost',
657 'type' => 'repost',
658 'collection' => 'reposts',
659 'activity_types' => array( 'announce' ),
660 'excerpt' => __( '&hellip; reposted this!', 'activitypub' ),
661 /* translators: %d: Number of reposts */
662 'count_single' => _x( '%d repost', 'number of reposts', 'activitypub' ),
663 /* translators: %d: Number of reposts */
664 'count_plural' => _x( '%d reposts', 'number of reposts', 'activitypub' ),
665 )
666 );
667
668 register_comment_type(
669 'like',
670 array(
671 'label' => __( 'Likes', 'activitypub' ),
672 'singular' => __( 'Like', 'activitypub' ),
673 'description' => __( 'A like is a popular webaction button and in some cases post type on various silos such as Facebook and Instagram.', 'activitypub' ),
674 'icon' => '👍',
675 'class' => 'p-like',
676 'type' => 'like',
677 'collection' => 'likes',
678 'activity_types' => array( 'like' ),
679 'excerpt' => __( '&hellip; liked this!', 'activitypub' ),
680 /* translators: %d: Number of likes */
681 'count_single' => _x( '%d like', 'number of likes', 'activitypub' ),
682 /* translators: %d: Number of likes */
683 'count_plural' => _x( '%d likes', 'number of likes', 'activitypub' ),
684 )
685 );
686 }
687
688 /**
689 * Show avatars on Activities if set.
690 *
691 * @param array $types List of avatar enabled comment types.
692 *
693 * @return array show avatars on Activities
694 */
695 public static function get_avatar_comment_types( $types ) {
696 $comment_types = self::get_comment_type_slugs();
697 $types = array_merge( $types, $comment_types );
698
699 return array_unique( $types );
700 }
701
702 /**
703 * Excludes likes and reposts from comment queries.
704 *
705 * @author Jan Boddez
706 *
707 * @see https://github.com/janboddez/indieblocks/blob/a2d59de358031056a649ee47a1332ce9e39d4ce2/includes/functions.php#L423-L432
708 *
709 * @param WP_Comment_Query $query Comment count.
710 */
711 public static function comment_query( $query ) {
712 if ( ! $query instanceof WP_Comment_Query ) {
713 return;
714 }
715
716 if ( is_admin() || ! is_singular() ) {
717 return;
718 }
719
720 if ( ! empty( $query->query_vars['type__in'] ) ) {
721 return;
722 }
723
724 // Exclude likes and reposts by the ActivityPub plugin.
725 $query->query_vars['type__not_in'] = self::get_comment_type_slugs();
726 }
727
728 /**
729 * Filter the comment status before it is set.
730 *
731 * @param string $approved The approved comment status.
732 * @param array $commentdata The comment data.
733 *
734 * @return boolean `true` if the comment is approved, `false` otherwise.
735 */
736 public static function pre_comment_approved( $approved, $commentdata ) {
737 if ( $approved || \is_wp_error( $approved ) ) {
738 return $approved;
739 }
740
741 if ( '1' !== \get_option( 'comment_previously_approved' ) ) {
742 return $approved;
743 }
744
745 if (
746 empty( $commentdata['comment_meta']['protocol'] ) ||
747 'activitypub' !== $commentdata['comment_meta']['protocol']
748 ) {
749 return $approved;
750 }
751
752 global $wpdb;
753
754 $author = $commentdata['comment_author'];
755 $author_url = $commentdata['comment_author_url'];
756 // phpcs:ignore
757 $ok_to_comment = $wpdb->get_var( $wpdb->prepare( "SELECT comment_approved FROM $wpdb->comments WHERE comment_author = %s AND comment_author_url = %s and comment_approved = '1' LIMIT 1", $author, $author_url ) );
758
759 if ( 1 === (int) $ok_to_comment ) {
760 return 1;
761 }
762
763 return $approved;
764 }
765
766 /**
767 * Filters the comment count to exclude ActivityPub comment types.
768 *
769 * @param int|null $new_count The new comment count. Default null.
770 * @param int $old_count The old comment count.
771 * @param int $post_id Post ID.
772 *
773 * @return int|null The updated comment count, or null to use the default query.
774 */
775 public static function pre_wp_update_comment_count_now( $new_count, $old_count, $post_id ) {
776 if ( null === $new_count ) {
777 global $wpdb;
778
779 $excluded_types = self::get_comment_type_slugs();
780
781 // phpcs:ignore WordPress.DB
782 $new_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type NOT IN ('" . implode( "','", $excluded_types ) . "')", $post_id ) );
783
784 }
785
786 return $new_count;
787 }
788 }
789