• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

timber / timber / 20695674007

04 Jan 2026 04:14PM UTC coverage: 89.681% (+1.5%) from 88.211%
20695674007

push

travis-ci

nlemoine
test: Fix ancestors post tests

4615 of 5146 relevant lines covered (89.68%)

63.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.45
/src/Comment.php
1
<?php
2

3
namespace Timber;
4

5
use Stringable;
6
use WP_Comment;
7

8
/**
9
 * Class Comment
10
 *
11
 * The `Timber\Comment` class is used to view the output of comments. 99% of the time this will be
12
 * in the context of the comments on a post. However you can also fetch a comment directly using its
13
 * comment ID.
14
 *
15
 * @api
16
 * @example
17
 * ```php
18
 * $comment = Timber::get_comment( $comment_id );
19
 *
20
 * $context = [
21
 *     'comment_of_the_day' => $comment
22
 * ];
23
 *
24
 * Timber::render('index.twig', $context);
25
 * ```
26
 *
27
 * ```twig
28
 * <p class="comment">{{comment_of_the_day.content}}</p>
29
 * <p class="comment-attribution">- {{comment.author.name}}</p>
30
 * ```
31
 *
32
 * ```html
33
 * <p class="comment">But, O Sarah! If the dead can come back to this earth and flit unseen around those they loved, I shall always be near you; in the garish day and in the darkest night -- amidst your happiest scenes and gloomiest hours - always, always; and if there be a soft breeze upon your cheek, it shall be my breath; or the cool air fans your throbbing temple, it shall be my spirit passing by.</p>
34
 * <p class="comment-attribution">- Sullivan Ballou</p>
35
 * ```
36
 */
37
class Comment extends CoreEntity implements Stringable
38
{
39
    /**
40
     * The underlying WordPress Core object.
41
     *
42
     * @since 2.0.0
43
     *
44
     * @var WP_Comment|null
45
     */
46
    protected ?WP_Comment $wp_object = null;
47

48
    public $object_type = 'comment';
49

50
    public static $representation = 'comment';
51

52
    /**
53
     * @api
54
     * @var int
55
     */
56
    public $ID;
57

58
    /**
59
     * @api
60
     * @var int
61
     */
62
    public $id;
63

64
    /**
65
     * @var int
66
     */
67
    public $comment_approved;
68

69
    /**
70
     * @api
71
     * @var string
72
     */
73
    public $comment_author_email;
74

75
    /**
76
     * @api
77
     * @var string
78
     */
79
    public $comment_content;
80

81
    /**
82
     * @api
83
     * @var string
84
     */
85
    public $comment_date;
86

87
    /**
88
     * @api
89
     * @var int
90
     */
91
    public $comment_ID;
92

93
    /**
94
     * @var int
95
     */
96
    public $comment_parent;
97

98
    /**
99
     * @api
100
     * @var int
101
     */
102
    public $user_id;
103

104
    /**
105
     * @api
106
     * @var int
107
     */
108
    public $post_id;
109

110
    /**
111
     * @api
112
     * @var string
113
     */
114
    public $comment_author;
115

116
    public $_depth = 0;
117

118
    protected $children = [];
119

120
    /**
121
     * Construct a Timber\Comment. This is protected to prevent direct instantiation,
122
     * which is no longer supported. Use `Timber::get_comment()` instead.
123
     *
124
     * @internal
125
     */
126
    protected function __construct()
68✔
127
    {
128
    }
68✔
129

130
    /**
131
     * Build a Timber\Comment. Do not call this directly. Use `Timber::get_comment()` instead.
132
     *
133
     * @internal
134
     * @param WP_Comment $wp_comment a native WP_Comment instance
135
     */
136
    public static function build(WP_Comment $wp_comment): static
68✔
137
    {
138
        $comment = new static();
68✔
139
        $comment->import($wp_comment);
68✔
140
        $comment->ID = $wp_comment->comment_ID;
68✔
141
        $comment->id = $wp_comment->comment_ID;
68✔
142
        $comment->wp_object = $wp_comment;
68✔
143

144
        return $comment;
68✔
145
    }
146

147
    /**
148
     * Gets the content.
149
     *
150
     * @api
151
     * @return string
152
     */
153
    public function __toString()
1✔
154
    {
155
        return $this->content();
1✔
156
    }
157

158
    /**
159
     * @internal
160
     * @param integer $cid
161
     */
162
    public function init($cid)
×
163
    {
164
        $comment_data = $cid;
×
165
        if (\is_integer($cid)) {
×
166
            $comment_data = \get_comment($cid);
×
167
        }
168
        $this->import($comment_data);
×
169
        $this->ID = $this->comment_ID;
×
170
        $this->id = $this->comment_ID;
×
171
    }
172

173
    /**
174
     * Gets the underlying WordPress Core object.
175
     *
176
     * @since 2.0.0
177
     *
178
     * @return WP_Comment|null
179
     */
180
    public function wp_object(): ?WP_Comment
1✔
181
    {
182
        return $this->wp_object;
1✔
183
    }
184

185
    /**
186
     * Gets the author.
187
     *
188
     * @api
189
     * @example
190
     * ```twig
191
     * <h3>Comments by...</h3>
192
     * <ol>
193
     * {% for comment in post.comments %}
194
     *     <li>{{comment.author.name}}, who is a {{comment.author.roles[0]}}</li>
195
     * {% endfor %}
196
     * </ol>
197
     * ```
198
     * ```html
199
     * <h3>Comments by...</h3>
200
     * <ol>
201
     *  <li>Jared Novack, who is a contributor</li>
202
     *  <li>Katie Ricci, who is a subscriber</li>
203
     *  <li>Rebecca Pearl, who is a author</li>
204
     * </ol>
205
     * ```
206
     * @return User
207
     */
208
    public function author()
6✔
209
    {
210
        if ($this->user_id) {
6✔
211
            return Timber::get_user($this->user_id);
4✔
212
        } else {
213
            // We can't (and shouldn't) construct a full-blown User object,
214
            // so just return a stdclass inst with a name
215
            return (object) [
2✔
216
                'name' => $this->comment_author ?: 'Anonymous',
2✔
217
            ];
2✔
218
        }
219
    }
220

221
    /**
222
     * Fetches the Gravatar.
223
     *
224
     * @api
225
     * @example
226
     * ```twig
227
     * <img src="{{comment.avatar(36,template_uri~"/img/dude.jpg")}}" alt="Image of {{comment.author.name}}" />
228
     * ```
229
     * ```html
230
     * <img src="https://gravatar.com/i/sfsfsdfasdfsfa.jpg" alt="Image of Katherine Rich" />
231
     * ```
232
     * @param int|mixed    $size     Size of avatar.
233
     * @param string       $default  Default avatar URL.
234
     * @return bool|mixed|string
235
     */
236
    public function avatar($size = 92, $default = '')
6✔
237
    {
238
        if (!\get_option('show_avatars')) {
6✔
239
            return false;
1✔
240
        }
241
        if (!\is_numeric($size)) {
5✔
242
            $size = '92';
1✔
243
        }
244

245
        $email = $this->avatar_email();
5✔
246

247
        $args = [
5✔
248
            'size' => $size,
5✔
249
            'default' => $default,
5✔
250
        ];
5✔
251
        $args = \apply_filters('pre_get_avatar_data', $args, $email);
5✔
252
        if (isset($args['url'])) {
5✔
253
            return $args['url'];
×
254
        }
255

256
        if (isset($args['default'])) {
5✔
257
            $default = $args['default'];
5✔
258
        }
259

260
        $email_hash = '';
5✔
261
        if (!empty($email)) {
5✔
262
            $email_hash = \md5(\strtolower(\trim($email)));
1✔
263
        }
264
        $host = $this->avatar_host($email_hash);
5✔
265
        $default = $this->avatar_default($default, $email, $size, $host);
5✔
266
        if (!empty($email)) {
5✔
267
            $avatar = $this->avatar_out($default, $host, $email_hash, $size);
1✔
268
        } else {
269
            $avatar = $default;
4✔
270
        }
271
        return $avatar;
5✔
272
    }
273

274
    /**
275
     * Gets the content.
276
     *
277
     * @api
278
     * @return string
279
     */
280
    public function content()
3✔
281
    {
282
        return \trim((string) \apply_filters('comment_text', $this->comment_content));
3✔
283
    }
284

285
    /**
286
     * Gets the comment children.
287
     *
288
     * @api
289
     * @return array Comments
290
     */
291
    public function children()
15✔
292
    {
293
        return $this->children;
15✔
294
    }
295

296
    /**
297
     * Adds a child.
298
     *
299
     * @api
300
     * @param Comment $child_comment Comment child to add.
301
     * @return array Comment children.
302
     */
303
    public function add_child(Comment $child_comment)
7✔
304
    {
305
        return $this->children[] = $child_comment;
7✔
306
    }
307

308
    /**
309
     * Updates the comment depth.
310
     *
311
     * @api
312
     * @param int $depth Level of depth.
313
     */
314
    public function update_depth($depth = 0)
15✔
315
    {
316
        $this->_depth = $depth;
15✔
317
        $children = $this->children();
15✔
318
        foreach ($children as $comment) {
15✔
319
            $child_depth = $depth + 1;
7✔
320
            $comment->update_depth($child_depth);
7✔
321
        }
322
    }
323

324
    /**
325
     * At what depth is this comment?
326
     *
327
     * @api
328
     * @return int
329
     */
330
    public function depth()
3✔
331
    {
332
        return $this->_depth;
3✔
333
    }
334

335
    /**
336
     * Is the comment approved?
337
     *
338
     * @api
339
     * @example
340
     * ```twig
341
     * {% if comment.approved %}
342
     *   Your comment is good
343
     * {% else %}
344
     *   Do you kiss your mother with that mouth?
345
     * {% endif %}
346
     * ```
347
     * @return boolean
348
     */
349
    public function approved()
1✔
350
    {
351
        return Helper::is_true($this->comment_approved);
1✔
352
    }
353

354
    /**
355
     * The date for the comment.
356
     *
357
     * @api
358
     * @example
359
     * ```twig
360
     * {% for comment in post.comments %}
361
     * <article class="comment">
362
     *   <p class="date">Posted on {{ comment.date }}:</p>
363
     *   <p class="comment">{{ comment.content }}</p>
364
     * </article>
365
     * {% endfor %}
366
     * ```
367
     * ```html
368
     * <article class="comment">
369
     *   <p class="date">Posted on September 28, 2015:</p>
370
     *   <p class="comment">Happy Birthday!</p>
371
     * </article>
372
     * ```
373
     * @param string $date_format of desired PHP date format (eg "M j, Y").
374
     * @return string
375
     */
376
    public function date($date_format = '')
1✔
377
    {
378
        $df = $date_format ?: \get_option('date_format');
1✔
379
        $the_date = (string) \mysql2date($df, $this->comment_date);
1✔
380
        return \apply_filters('get_comment_date ', $the_date, $df);
1✔
381
    }
382

383
    /**
384
     * What time was the comment posted?
385
     *
386
     * @api
387
     * @example
388
     * ```twig
389
     * {% for comment in post.comments %}
390
     * <article class="comment">
391
     *   <p class="date">Posted on {{ comment.date }} at {{comment.time}}:</p>
392
     *   <p class="comment">{{ comment.content }}</p>
393
     * </article>
394
     * {% endfor %}
395
     * ```
396
     * ```html
397
     * <article class="comment">
398
     *   <p class="date">Posted on September 28, 2015 at 12:45 am:</p>
399
     *   <p class="comment">Happy Birthday!</p>
400
     * </article>
401
     * ```
402
     * @param string $time_format of desired PHP time format (eg "H:i:s").
403
     * @return string
404
     */
405
    public function time($time_format = '')
1✔
406
    {
407
        $tf = $time_format ?: \get_option('time_format');
1✔
408
        $the_time = (string) \mysql2date($tf, $this->comment_date);
1✔
409
        return \apply_filters('get_comment_time', $the_time, $tf);
1✔
410
    }
411

412
    /**
413
     * Gets a comment meta value.
414
     *
415
     * @api
416
     * @deprecated 2.0.0, use `{{ comment.meta('field_name') }}` instead.
417
     *
418
     * @param string $field_name The field name for which you want to get the value.
419
     * @return mixed The meta field value.
420
     */
421
    public function get_meta_field($field_name)
×
422
    {
423
        Helper::deprecated(
×
424
            "{{ comment.get_meta_field('field_name') }}",
×
425
            "{{ comment.meta('field_name') }}",
×
426
            '2.0.0'
×
427
        );
×
428

429
        return $this->meta($field_name);
×
430
    }
431

432
    /**
433
     * Checks if the comment is a child.
434
     *
435
     * @api
436
     * @return bool
437
     */
438
    public function is_child()
15✔
439
    {
440
        return $this->comment_parent > 0;
15✔
441
    }
442

443
    /**
444
     * Gets a comment meta value.
445
     *
446
     * @api
447
     * @deprecated 2.0.0, use `{{ comment.meta('field_name') }}` instead.
448
     * @see \Timber\Comment::meta()
449
     *
450
     * @param string $field_name The field name for which you want to get the value.
451
     * @return mixed The meta field value.
452
     */
453
    public function get_field($field_name = null)
1✔
454
    {
455
        Helper::deprecated(
1✔
456
            "{{ comment.get_field('field_name') }}",
1✔
457
            "{{ comment.meta('field_name') }}",
1✔
458
            '2.0.0'
1✔
459
        );
1✔
460

461
        return $this->meta($field_name);
1✔
462
    }
463

464
    /**
465
     * Enqueue the WP threaded comments JavaScript, and fetch the reply link for various comments.
466
     *
467
     * @api
468
     * @param string $reply_text Text of the reply link.
469
     * @return string
470
     */
471
    public function reply_link($reply_text = 'Reply')
1✔
472
    {
473
        if (\is_singular() && \comments_open() && \get_option('thread_comments')) {
1✔
474
            \wp_enqueue_script('comment-reply');
×
475
        }
476

477
        // Get the comments depth option from the admin panel
478
        $max_depth = \get_option('thread_comments_depth');
1✔
479

480
        // Default args
481
        $args = [
1✔
482
            'add_below' => 'comment',
1✔
483
            'respond_id' => 'respond',
1✔
484
            'reply_text' => $reply_text,
1✔
485
            'depth' => $this->depth() + 1,
1✔
486
            'max_depth' => $max_depth,
1✔
487
        ];
1✔
488

489
        return \get_comment_reply_link($args, $this->ID, $this->post_id);
1✔
490
    }
491

492
    /**
493
     * Checks whether the current user can edit the comment.
494
     *
495
     * @api
496
     * @example
497
     * ```twig
498
     * {% if comment.can_edit %}
499
     *     <a href="{{ comment.edit_link }}">Edit</a>
500
     * {% endif %}
501
     * ```
502
     * @return bool
503
     */
504
    public function can_edit(): bool
2✔
505
    {
506
        return \current_user_can('edit_comment', $this->ID);
2✔
507
    }
508

509
    /**
510
     * Gets the edit link for a comment if the current user has the correct rights.
511
     *
512
     * @api
513
     * @since 2.0.0
514
     * @example
515
     * ```twig
516
     * {% if comment.can_edit %}
517
     *     <a href="{{ comment.edit_link }}">Edit</a>
518
     * {% endif %}
519
     * ```
520
     * @return string|null The edit URL of a comment in the WordPress admin or null if the current user can’t edit the
521
     *                     comment.
522
     */
523
    public function edit_link(): ?string
1✔
524
    {
525
        if (!$this->can_edit()) {
1✔
526
            return null;
1✔
527
        }
528

529
        return \get_edit_comment_link($this->ID);
1✔
530
    }
531

532
    /* AVATAR Stuff
533
    ======================= */
534

535
    /**
536
     * @internal
537
     * @return string
538
     */
539
    protected function avatar_email()
5✔
540
    {
541
        $id = (int) $this->user_id;
5✔
542
        $user = \get_userdata($id);
5✔
543
        if ($user) {
5✔
544
            $email = $user->user_email;
×
545
        } else {
546
            $email = $this->comment_author_email;
5✔
547
        }
548
        return $email;
5✔
549
    }
550

551
    /**
552
     * @internal
553
     * @param string $email_hash
554
     * @return string
555
     */
556
    protected function avatar_host($email_hash)
5✔
557
    {
558
        if (\is_ssl()) {
5✔
559
            $host = 'https://secure.gravatar.com';
1✔
560
        } else {
561
            if (!empty($email_hash)) {
5✔
562
                $host = \sprintf("https://%d.gravatar.com", (\hexdec($email_hash[0]) % 2));
1✔
563
            } else {
564
                $host = 'https://0.gravatar.com';
4✔
565
            }
566
        }
567
        return $host;
5✔
568
    }
569

570
    /**
571
     * @internal
572
     * @param string $default
573
     * @param string $email
574
     * @param string $size
575
     * @param string $host
576
     * @return string
577
     */
578
    protected function avatar_default($default, $email, $size, $host)
5✔
579
    {
580
        if (\str_starts_with($default, '/')) {
5✔
581
            $default = \home_url() . $default;
×
582
        }
583

584
        if (empty($default)) {
5✔
585
            $avatar_default = \get_option('avatar_default');
2✔
586
            if (empty($avatar_default)) {
2✔
587
                $default = 'mystery';
×
588
            } else {
589
                $default = $avatar_default;
2✔
590
            }
591
        }
592

593
        if ('mystery' == $default) {
5✔
594
            $default = $host . '/avatar/ad516503a11cd5ca435acc9bb6523536?s=' . $size;
3✔
595
            // ad516503a11cd5ca435acc9bb6523536 == md5('unknown@gravatar.com')
596
        } elseif ('blank' == $default) {
3✔
597
            $default = $email ? 'blank' : \includes_url('images/blank.gif');
1✔
598
        } elseif (!empty($email) && 'gravatar_default' == $default) {
2✔
599
            $default = '';
×
600
        } elseif ('gravatar_default' == $default) {
2✔
601
            $default = $host . '/avatar/?s=' . $size;
1✔
602
        } elseif (empty($email) && !\preg_match('/^https?:\/\//', (string) $default)) {
1✔
603
            $default = $host . '/avatar/?d=' . $default . '&amp;s=' . $size;
×
604
        }
605
        return $default;
5✔
606
    }
607

608
    /**
609
     * @internal
610
     * @param string $default
611
     * @param string $host
612
     * @param string $email_hash
613
     * @param string $size
614
     * @return mixed
615
     */
616
    protected function avatar_out($default, $host, $email_hash, $size)
1✔
617
    {
618
        $out = $host . '/avatar/' . $email_hash;
1✔
619
        $rating = \get_option('avatar_rating');
1✔
620

621
        $url_args = [
1✔
622
            's' => $size,
1✔
623
            'd' => $default,
1✔
624
        ];
1✔
625

626
        if (!empty($rating)) {
1✔
627
            $url_args['r'] = $rating;
1✔
628
        }
629

630
        $out = \add_query_arg(
1✔
631
            \rawurlencode_deep(\array_filter($url_args)),
1✔
632
            $out
1✔
633
        );
1✔
634

635
        return \str_replace('&#038;', '&amp;', (string) \esc_url($out));
1✔
636
    }
637
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc