Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 265
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_Widget_Recent_Comments
0.00% covered (danger)
0.00%
0 / 258
0.00% covered (danger)
0.00%
0 / 9
3422
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 style
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
2
 flush_cache
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_cache_key
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 is_post_public
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
90
 widget
0.00% covered (danger)
0.00%
0 / 134
0.00% covered (danger)
0.00%
0 / 1
1122
 form
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
30
 update
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 get_allowed_post_types
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
1<?php // phpcs:ignore Squiz.Commenting.FileComment.MissingPackageTag
2/**
3 * File copied from WP.com
4 */
5
6add_action(
7    'widgets_init',
8    function () {
9        unregister_widget( 'WP_Widget_Recent_Comments' );
10
11        register_widget( 'WPCOM_Widget_Recent_Comments' );
12    }
13);
14
15/**
16 * A WordPress.com Reservations widget.
17 */
18class WPCOM_Widget_Recent_Comments extends WP_Widget {
19    /**
20     * Alt option name.
21     *
22     * @var string $alt_option_name
23     */
24    public $alt_option_name = 'widget_recent_comments';
25
26    /**
27     * Allowed post types.
28     *
29     * @var string[] $allowed_post_types
30     */
31    private static $allowed_post_types = null;
32
33    /**
34     * Widget default settings.
35     *
36     * @var array $widget_defaults
37     */
38    protected static $widget_defaults = array(
39        'title'       => '',
40        'number'      => 5,
41        'avatar_size' => 48,
42        'avatar_bg'   => '',
43        'text_bg'     => '',
44        'post_types'  => array( 'post' ),
45    );
46
47    /**
48     * Constructor
49     */
50    public function __construct() {
51        parent::__construct(
52            'recent-comments',
53            __( 'Recent Comments', 'wpcomsh' ),
54            array(
55                'classname'   => 'widget_recent_comments',
56                'description' => __( 'Display your site\'s most recent comments', 'wpcomsh' ),
57            )
58        );
59
60        if ( is_active_widget( false, false, 'recent-comments', false ) ) {
61            add_action( 'wp_head', array( $this, 'style' ) );
62
63            add_action( 'comment_post', array( $this, 'flush_cache' ) );
64            add_action( 'wp_set_comment_status', array( $this, 'flush_cache' ) );
65            add_action( 'transition_comment_status', array( $this, 'flush_cache' ) );
66        }
67    }
68
69    /**
70     * Output style tag for widget.
71     */
72    public function style() {
73        ?>
74        <style type="text/css">
75            .recentcomments a {
76                display: inline !important;
77                padding: 0 !important;
78                margin: 0 !important;
79            }
80
81            table.recentcommentsavatartop img.avatar, table.recentcommentsavatarend img.avatar {
82                border: 0;
83                margin: 0;
84            }
85
86            table.recentcommentsavatartop a, table.recentcommentsavatarend a {
87                border: 0 !important;
88                background-color: transparent !important;
89            }
90
91            td.recentcommentsavatarend, td.recentcommentsavatartop {
92                padding: 0 0 1px 0;
93                margin: 0;
94            }
95
96            td.recentcommentstextend {
97                border: none !important;
98                padding: 0 0 2px 10px;
99            }
100
101            .rtl td.recentcommentstextend {
102                padding: 0 10px 2px 0;
103            }
104
105            td.recentcommentstexttop {
106                border: none;
107                padding: 0 0 0 10px;
108            }
109
110            .rtl td.recentcommentstexttop {
111                padding: 0 10px 0 0;
112            }
113        </style>
114        <?php
115    }
116
117    /**
118     * Flush cache.
119     */
120    public function flush_cache() {
121        wp_cache_set( 'recent-comments-cache-buster', time(), 'widget' );
122    }
123
124    /**
125     * Get cache key.
126     */
127    protected function get_cache_key() {
128        $cache_buster = wp_cache_get( 'recent-comments-cache-buster', 'widget' );
129
130        if ( $cache_buster === false ) {
131            $cache_buster = time();
132            wp_cache_add( 'recent-comments-cache-buster', $cache_buster, 'widget' );
133        }
134
135        // recent-comments-{number}-{timestamp}
136        return "{$this->id}-{$cache_buster}";
137    }
138
139    /**
140     * Check if post is public.
141     *
142     * @param WP_Post            $post Post object.
143     * @param array|string|false $post_types Post type(s) that should have comments.
144     *
145     * @return bool True if post checks out, otherwise false.
146     */
147    public function is_post_public( $post, $post_types = false ) {
148        // For what post types should we display comments?
149        if ( is_string( $post_types ) ) {
150            $post_types = array( $post_types );
151        } elseif ( ! is_array( $post_types ) || empty( $post_types ) ) {
152            $post_types = self::$widget_defaults['post_types'];
153        }
154
155        $post_types = array_intersect( $post_types, array_keys( $this->get_allowed_post_types() ) );
156
157        // If we're dealing with a comment on an attachment page, base its display off of the parent post's status, but only if it has a parent.
158        if ( 'attachment' === $post->post_type ) {
159            if ( ! in_array( 'attachment', $post_types, true ) ) {
160                return false;
161            }
162
163            $parent_post = get_post( $post->post_parent );
164            if ( ! empty( $parent_post ) ) {
165                $post = $parent_post;
166            }
167        }
168
169        // Limit to comments on chosen types of content
170        if ( ! in_array( $post->post_type, $post_types, true ) ) {
171            return false;
172        }
173
174        if ( ! empty( $post->post_password ) ) {
175            return false;
176        }
177
178        // Hooray, all of our checks passed!
179        return true;
180    }
181
182    /**
183     * Display the widget.
184     *
185     * @param array $args     Widget arguments.
186     * @param array $instance Widget instance.
187     */
188    public function widget( $args, $instance ) {
189        $instance = wp_parse_args( $instance, self::$widget_defaults );
190
191        if ( empty( $instance['title'] ) ) {
192            $instance['title'] = __( 'Recent Comments', 'wpcomsh' );
193        } else {
194            $instance['title'] = apply_filters( 'widget_title', $instance['title'] );
195        }
196
197        $instance['number'] = (int) $instance['number'];
198
199        if ( ! $instance['number'] ) {
200            $instance['number'] = 5;
201        } else {
202            $instance['number'] = max( 1, min( 15, $instance['number'] ) );
203        }
204
205        $instance['avatar_size'] = (int) $instance['avatar_size'] ? (int) $instance['avatar_size'] : 48;
206
207        $comments = wp_cache_get( $this->get_cache_key(), 'widget' );
208
209        if ( ! $comments ) {
210            $comments        = array();
211            $comments_offset = 0;
212            $loop_counter    = 0;
213            $number_of_items = 30;
214
215            // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
216            while ( $comments_scoop = get_comments(
217                array(
218                    'number' => $number_of_items,
219                    'offset' => $comments_offset,
220                    'status' => 'approve',
221                )
222            ) ) {
223                $posts    = get_posts(
224                    array(
225                        'ignore_sticky_posts' => true,
226                        'orderby'             => 'post__in',
227                        'posts_per_page'      => $number_of_items,
228                        'post_type'           => $instance['post_types'],
229                        'post__in'            => array_unique( wp_list_pluck( $comments_scoop, 'comment_post_ID' ) ),
230                        'suppress_filters'    => false,
231                        'post_status'         => array( 'inherit', 'publish' ),
232                    )
233                );
234                $post_ids = wp_list_pluck( $posts, 'ID' );
235
236                // get_posts() will not fetch private posts or invalid post types, despite post__in. Filter those out of $comments_scoop.
237                if ( $post_ids ) {
238                    foreach ( $comments_scoop as $index => $comment ) {
239                        if ( ! in_array( (int) $comment->comment_post_ID, $post_ids, true ) ) {
240                            unset( $comments_scoop[ $index ] );
241                        }
242                    }
243
244                    // If no posts were found, we either: 1) requested comments for wrong post type or status, or 2) found orphaned comments.
245                } else {
246                    $comments_scoop = array();
247                }
248
249                // Do we have any comments that've passed our initial scrutiny?
250                if ( $comments_scoop ) {
251                    foreach ( $posts as $post ) {
252                        if ( ! self::is_post_public( $post, $instance['post_types'] ) ) {
253                            $comments_scoop = wp_filter_object_list( $comments_scoop, array( 'comment_post_ID' => $post->ID ), 'NOT' );
254                        }
255                    }
256                }
257
258                $comments = array_merge( $comments, $comments_scoop );
259
260                // Do we have enough comments yet?
261                if ( count( $comments ) >= $instance['number'] ) {
262
263                    // We do. Trim out exactly how many we want, and stop looping.
264                    $comments = array_slice( $comments, 0, $instance['number'] );
265                    break;
266                }
267
268                $comments_offset += $number_of_items;
269                ++$loop_counter;
270
271                // Prevent sites with many comments and many blog posts causing many queries for certain widget configs.
272                if ( $loop_counter >= 3 ) {
273                    break;
274                }
275            }
276
277            if ( ! empty( $comments ) ) {
278                wp_cache_set( $this->get_cache_key(), $comments, 'widget' );
279            }
280        }
281
282        echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
283        echo $args['before_title'] . esc_html( $instance['title'] ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
284
285        if ( $comments ) {
286            if ( isset( $instance['avatar_size'] ) && $instance['avatar_size'] != 1 ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
287                $avatar_bg = empty( $instance['avatar_bg'] ) ? '' : 'background: ' . $instance['avatar_bg'] . ';';
288                $text_bg   = empty( $instance['text_bg'] ) ? '' : 'background: ' . $instance['text_bg'] . ';';
289
290                ?>
291                <table class="recentcommentsavatar" cellspacing="0" cellpadding="0" border="0">
292                    <?php
293                    $comments_printed = 0;
294
295                    foreach ( $comments as $comment_index => $comment ) {
296                        if ( $instance['number'] <= $comments_printed ) {
297                            break;
298                        }
299
300                        $avatar = get_avatar( $comment, $instance['avatar_size'] );
301
302                        if ( $comment->comment_author_url ) {
303                            $avatar = '<a href="' . esc_url( $comment->comment_author_url ) . '" rel="nofollow">' . $avatar . '</a>';
304                        }
305
306                        echo '<tr><td title="' . esc_attr( $comment->comment_author ) . '" class="' . esc_attr( $comment_index === 0 ? 'recentcommentsavatartop' : 'recentcommentsavatarend' ) . '" style="' . esc_attr( 'height:' . $instance['avatar_size'] . 'px; width:' . $instance['avatar_size'] . 'px;' . $avatar_bg ) . '">';
307                        echo $avatar; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- as $avatar has <img> tag.
308                        echo '</td>';
309                        echo '<td class="' . esc_attr( $comment_index === 0 ? 'recentcommentstexttop' : 'recentcommentstextend' ) . '" style="' . esc_attr( $text_bg ) . '">';
310
311                        if ( $comment->comment_author === '' ) {
312                            $comment->comment_author = __( 'Anonymous', 'wpcomsh' );
313                        }
314
315                        $author  = $comment->comment_author;
316                        $excerpt = wp_html_excerpt( $author, 20 );
317
318                        if ( $author !== $excerpt ) {
319                            $author = $excerpt . '&hellip;';
320                        }
321
322                        if ( $comment->comment_author_url === '' ) {
323                            $authorlink = esc_html( $author );
324                        } else {
325                            $authorlink = '<a href="' . esc_url( $comment->comment_author_url ) . '" rel="nofollow">' . esc_html( $author ) . '</a>';
326                        }
327
328                        $post_title = get_the_title( (int) $comment->comment_post_ID );
329                        $excerpt    = wp_html_excerpt( $post_title, 30 );
330
331                        if ( $post_title !== $excerpt ) {
332                            $post_title = $excerpt . '&hellip;';
333                        }
334
335                        if ( empty( $post_title ) ) {
336                            $post_title = '&hellip;';
337                        }
338
339                        printf(
340                            wp_kses(
341                                /* translators: comments widget: 1: comment author, 2: comment link, 3: comment title */
342                                _x( '%1$s on <a href="%2$s">%3$s</a>', 'widgets', 'wpcomsh' ),
343                                array(
344                                    'a' => array(
345                                        'href' => array(),
346                                        'rel'  => array(),
347                                    ),
348                                )
349                            ),
350                            $authorlink, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- may contain HTML but already escaped
351                            esc_url( get_comment_link( $comment ) ),
352                            esc_html( $post_title )
353                        );
354
355                        echo '</td></tr>';
356
357                        ++$comments_printed;
358                    }
359
360                    if ( 0 === $comments_printed ) {
361                        echo '<tr><td class="recentcommentstexttop" style="' . esc_attr( $text_bg ) . '">';
362                        esc_html_e( 'There are no public comments available to display.', 'wpcomsh' );
363                        echo '</td></tr>';
364                    }
365
366                    ?>
367                </table>
368                <?php
369
370            } else {
371                ?>
372                <ul id="recentcomments">
373                    <?php
374                    $comments_printed = 0;
375
376                    foreach ( $comments as $comment ) {
377                        if ( $instance['number'] <= $comments_printed ) {
378                            break;
379                        }
380                        ?>
381                        <li class="recentcomments">
382                            <?php
383
384                            printf(
385                                wp_kses(
386                                    /* translators: comments widget: 1: comment author link HTML, 2: comment link, 3: comment title */
387                                    _x( '%1$s on <a href="%2$s">%3$s</a>', 'widgets', 'wpcomsh' ),
388                                    array(
389                                        'a' => array( 'href' => array() ),
390                                    )
391                                ),
392                                get_comment_author_link( (int) $comment->comment_ID ), // HTML generated by WordPress
393                                esc_url( get_comment_link( $comment ) ),
394                                esc_html( get_the_title( (int) $comment->comment_post_ID ) )
395                            );
396
397                            ?>
398                        </li>
399
400                        <?php
401                        ++$comments_printed;
402                    }
403
404                    ?>
405                </ul>
406                <?php
407            }
408        }
409
410        echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
411    }
412
413    /**
414     * Display the widget settings form.
415     *
416     * @param array $instance Current settings.
417     * @return never
418     */
419    public function form( $instance ) {
420        $instance = wp_parse_args( $instance, self::$widget_defaults );
421
422        // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
423        if ( $instance['number'] != (int) $instance['number'] ) {
424            $instance['number'] = 5;
425        }
426
427        $instance['number'] = max( 1, min( 15, $instance['number'] ) );
428
429        if ( empty( $instance['avatar_size'] ) ) {
430            $instance['avatar_size'] = 48;
431        }
432
433        ?>
434        <p>
435            <label>
436                <?php esc_html_e( 'Title:', 'wpcomsh' ); ?>
437                <input class="widefat" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>" />
438            </label>
439        </p>
440        <p>
441            <label>
442                <?php esc_html_e( 'Number of comments to show:', 'wpcomsh' ); ?>
443                <select name="<?php echo esc_attr( $this->get_field_name( 'number' ) ); ?>">
444                    <?php for ( $i = 1; $i <= 15; $i++ ) : ?>
445                        <option value="<?php echo (int) $i; ?><?php selected( $instance['number'], $i ); ?>><?php echo (int) $i; /* @phan-suppress-current-line PhanRedundantConditionInLoop -- phpcs needs the explicit cast. */ ?></option>
446                    <?php endfor; ?>
447                </select>
448                <small><?php esc_html_e( '(at most 15)', 'wpcomsh' ); ?></small>
449            </label>
450        </p>
451        <p>
452            <label>
453                <?php esc_html_e( 'Avatar Size (px):', 'wpcomsh' ); ?>
454                <select name="<?php echo esc_attr( $this->get_field_name( 'avatar_size' ) ); ?>">
455                    <option value="1" <?php selected( $instance['avatar_size'], 1 ); ?>><?php esc_html_e( 'No Avatars', 'wpcomsh' ); ?></option>
456                    <option value="16" <?php selected( $instance['avatar_size'], 16 ); ?>>16x16</option>
457                    <option value="32" <?php selected( $instance['avatar_size'], 32 ); ?>>32x32</option>
458                    <option value="48" <?php selected( $instance['avatar_size'], 48 ); ?>>48x48</option>
459                    <option value="96" <?php selected( $instance['avatar_size'], 96 ); ?>>96x96</option>
460                    <option value="128" <?php selected( $instance['avatar_size'], 128 ); ?>>128x128</option>
461                </select>
462            </label>
463        </p>
464        <p>
465            <label>
466                <?php esc_html_e( 'Avatar background color:', 'wpcomsh' ); ?>
467                <input name="<?php echo esc_attr( $this->get_field_name( 'avatar_bg' ) ); ?>" type="text" value="<?php echo esc_attr( $instance['avatar_bg'] ); ?>" size="3" />
468            </label>
469        </p>
470        <p>
471            <label>
472                <?php esc_html_e( 'Text background color:', 'wpcomsh' ); ?>
473                <input name="<?php echo esc_attr( $this->get_field_name( 'text_bg' ) ); ?>" type="text" value="<?php echo esc_attr( $instance['text_bg'] ); ?>" size="3" />
474            </label>
475        </p>
476        <p>
477            <label><?php esc_html_e( 'Show comments from:', 'wpcomsh' ); ?></label><br />
478
479            <?php foreach ( $this->get_allowed_post_types() as $post_type => $label ) : ?>
480                <input type="checkbox" name="<?php echo esc_attr( $this->get_field_name( 'post_types' ) ); ?>[]" id="<?php echo esc_attr( $this->get_field_id( 'post_types' ) ); ?>-<?php echo esc_attr( $post_type ); ?>" value="<?php echo esc_attr( $post_type ); ?>"<?php checked( in_array( $post_type, (array) $instance['post_types'], true ) ); ?> /> <label for="<?php echo esc_attr( $this->get_field_id( 'post_types' ) ); ?>-<?php echo esc_attr( $post_type ); ?>"><?php echo esc_html( $label ); // Don't translate as it's already translated. ?></label><br />
481            <?php endforeach; ?>
482        </p>
483        <?php
484    }
485
486    /**
487     * Update the widget settings.
488     *
489     * @param array $new_instance New settings.
490     * @param array $old_instance Old settings.
491     */
492    public function update( $new_instance, $old_instance ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
493        $new_instance['title']       = wp_strip_all_tags( $new_instance['title'] );
494        $new_instance['number']      = (int) $new_instance['number'];
495        $new_instance['avatar_size'] = (int) $new_instance['avatar_size'];
496        $new_instance['avatar_bg']   = preg_replace( '/[^a-z0-9#]/i', '', $new_instance['avatar_bg'] );
497        $new_instance['text_bg']     = preg_replace( '/[^a-z0-9#]/i', '', $new_instance['text_bg'] );
498        $new_instance['post_types']  = array_intersect( $new_instance['post_types'], array_keys( $this->get_allowed_post_types() ) );
499
500        $this->flush_cache();
501
502        return $new_instance;
503    }
504
505    /**
506     * Retrieve list of public post types that can have their comments displayed
507     *
508     * Returned array is keyed by the post type name, with its translated label as the value
509     *
510     * @return array
511     */
512    protected function get_allowed_post_types() {
513        if ( self::$allowed_post_types === null ) {
514            $post_types = get_post_types(
515                array(
516                    'public' => true,
517                ),
518                'objects'
519            );
520
521            // Only those post types that support comments should be considered. :)
522            foreach ( $post_types as $post_type => $object ) {
523                if ( post_type_supports( $post_type, 'comments' ) ) {
524                    $post_types[ $post_type ] = $object->labels->name;
525                } else {
526                    unset( $post_types[ $post_type ] );
527                }
528            }
529
530            self::$allowed_post_types = $post_types;
531            unset( $post_types );
532        }
533
534        return self::$allowed_post_types;
535    }
536}