Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.86% covered (warning)
56.86%
29 / 51
50.00% covered (danger)
50.00%
9 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContentCommentItem
56.86% covered (warning)
56.86%
29 / 51
50.00% covered (danger)
50.00%
9 / 18
90.93
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 jsonSerialize
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getBodyFragment
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getBodyHTML
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getBodyText
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMentions
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 getSignatureRanges
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTimestampRanges
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBodyRange
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAuthor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDisplayName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeading
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSubscribableHeading
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addSignatureRange
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSignatureRanges
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setAuthor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
4
5use DateTimeImmutable;
6use MediaWiki\Extension\DiscussionTools\CommentModifier;
7use MediaWiki\Extension\DiscussionTools\CommentUtils;
8use MediaWiki\Extension\DiscussionTools\ImmutableRange;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Parser\Sanitizer;
11use MediaWiki\Title\Title;
12use Wikimedia\Parsoid\DOM\DocumentFragment;
13use Wikimedia\Parsoid\DOM\Text;
14use Wikimedia\Parsoid\Utils\DOMCompat;
15use Wikimedia\Parsoid\Utils\DOMUtils;
16
17class ContentCommentItem extends ContentThreadItem implements CommentItem {
18    use CommentItemTrait {
19        getHeading as protected traitGetHeading;
20        getSubscribableHeading as protected traitGetSubscribableHeading;
21        jsonSerialize as protected traitJsonSerialize;
22    }
23
24    /** @var ImmutableRange[] */
25    private array $signatureRanges;
26    /** @var ImmutableRange[] */
27    private array $timestampRanges;
28    private DateTimeImmutable $timestamp;
29    private string $author;
30    private ?string $displayName;
31
32    /**
33     * @param int $level
34     * @param ImmutableRange $range
35     * @param bool|string $transcludedFrom
36     * @param ImmutableRange[] $signatureRanges Objects describing the extent of signatures (plus
37     *  timestamps) for this comment. There is always at least one signature, but there may be
38     *  multiple. The author and timestamp of the comment is determined from the first signature.
39     * @param ImmutableRange[] $timestampRanges Objects describing the extent of timestamps within
40     *  the above signatures.
41     * @param DateTimeImmutable $timestamp
42     * @param string $author Comment author's username
43     * @param ?string $displayName Comment author's display name
44     */
45    public function __construct(
46        int $level, ImmutableRange $range, $transcludedFrom,
47        array $signatureRanges, array $timestampRanges,
48        DateTimeImmutable $timestamp,
49        string $author, ?string $displayName = null
50    ) {
51        parent::__construct( 'comment', $level, $range, $transcludedFrom );
52        $this->signatureRanges = $signatureRanges;
53        $this->timestampRanges = $timestampRanges;
54        $this->timestamp = $timestamp;
55        $this->author = $author;
56        $this->displayName = $displayName;
57    }
58
59    /**
60     * @inheritDoc CommentItemTrait::jsonSerialize
61     */
62    public function jsonSerialize( bool $deep = false, ?callable $callback = null ): array {
63        $data = $this->traitJsonSerialize( $deep, $callback );
64        if ( $this->displayName ) {
65            $data['displayName'] = $this->displayName;
66        }
67        return $data;
68    }
69
70    /**
71     * Get the DOM of this comment's body
72     *
73     * @param bool $stripTrailingSeparator Strip a trailing separator between the body and
74     *  the signature which consists of whitespace and hyphens e.g. ' --'
75     * @return DocumentFragment Cloned fragment of the body content
76     */
77    private function getBodyFragment( bool $stripTrailingSeparator = false ): DocumentFragment {
78        $fragment = $this->getBodyRange()->cloneContents();
79        CommentModifier::unwrapFragment( $fragment );
80
81        if ( $stripTrailingSeparator ) {
82            // Find a trailing text node
83            $lastChild = $fragment->lastChild;
84            while (
85                $lastChild &&
86                !( $lastChild instanceof Text )
87            ) {
88                $lastChild = $lastChild->lastChild;
89            }
90            if (
91                $lastChild instanceof Text &&
92                preg_match( '/[\s\-~\x{2010}-\x{2015}\x{2043}\x{2060}]+$/u', $lastChild->nodeValue ?? '', $matches )
93            ) {
94                $lastChild->nodeValue =
95                    substr( $lastChild->nodeValue ?? '', 0, -strlen( $matches[0] ) );
96            }
97        }
98        return $fragment;
99    }
100
101    /**
102     * Get the HTML of this comment's body
103     *
104     *
105     * @param bool $stripTrailingSeparator See getBodyFragment
106     * @return string HTML
107     */
108    public function getBodyHTML( bool $stripTrailingSeparator = false ): string {
109        $fragment = $this->getBodyFragment( $stripTrailingSeparator );
110        return DOMUtils::getFragmentInnerHTML( $fragment );
111    }
112
113    /**
114     * Get the text of this comment's body
115     *
116     * @param bool $stripTrailingSeparator See getBodyFragment
117     * @return string Text
118     */
119    public function getBodyText( bool $stripTrailingSeparator = false ): string {
120        $html = $this->getBodyHTML( $stripTrailingSeparator );
121        return Sanitizer::stripAllTags( $html );
122    }
123
124    /**
125     * Get a list of all users mentioned
126     *
127     * @return Title[] Title objects for mentioned user pages
128     */
129    public function getMentions(): array {
130        $fragment = $this->getBodyRange()->cloneContents();
131        // Note: DOMCompat::getElementsByTagName() doesn't take a DocumentFragment argument
132        $links = DOMCompat::querySelectorAll( $fragment, 'a' );
133        $users = [];
134        foreach ( $links as $link ) {
135            $href = $link->getAttribute( 'href' );
136            if ( $href ) {
137                $siteConfig = MediaWikiServices::getInstance()->getMainConfig();
138                $title = Title::newFromText( CommentUtils::getTitleFromUrl( $href, $siteConfig ) );
139                if ( $title && $title->inNamespace( NS_USER ) ) {
140                    // TODO: Consider returning User objects
141                    $users[] = $title;
142                }
143            }
144        }
145        return array_unique( $users );
146    }
147
148    /**
149     * @return ImmutableRange[] Comment signature ranges
150     */
151    public function getSignatureRanges(): array {
152        return $this->signatureRanges;
153    }
154
155    /**
156     * @return ImmutableRange[] Comment timestamp ranges
157     */
158    public function getTimestampRanges(): array {
159        return $this->timestampRanges;
160    }
161
162    /**
163     * @return ImmutableRange Range of the comment's "body"
164     */
165    public function getBodyRange(): ImmutableRange {
166        // Exclude last signature from body
167        $signatureRanges = $this->getSignatureRanges();
168        $lastSignature = end( $signatureRanges );
169        return $this->getRange()->setEnd( $lastSignature->startContainer, $lastSignature->startOffset );
170    }
171
172    /**
173     * @return DateTimeImmutable Comment timestamp
174     */
175    public function getTimestamp(): DateTimeImmutable {
176        return $this->timestamp;
177    }
178
179    /**
180     * @return string Comment author
181     */
182    public function getAuthor(): string {
183        return $this->author;
184    }
185
186    /**
187     * @return ?string Comment display name
188     */
189    public function getDisplayName(): ?string {
190        return $this->displayName;
191    }
192
193    /**
194     * @inheritDoc CommentItemTrait::getHeading
195     * @suppress PhanTypeMismatchReturnSuperType
196     */
197    public function getHeading(): ContentHeadingItem {
198        return $this->traitGetHeading();
199    }
200
201    /**
202     * @inheritDoc CommentItemTrait::getSubscribableHeading
203     */
204    public function getSubscribableHeading(): ?ContentHeadingItem {
205        return $this->traitGetSubscribableHeading();
206    }
207
208    /**
209     * @param ImmutableRange $signatureRange Comment signature range to add
210     */
211    public function addSignatureRange( ImmutableRange $signatureRange ): void {
212        $this->signatureRanges[] = $signatureRange;
213    }
214
215    /**
216     * @param ImmutableRange[] $signatureRanges Comment signature ranges
217     */
218    public function setSignatureRanges( array $signatureRanges ): void {
219        $this->signatureRanges = $signatureRanges;
220    }
221
222    /**
223     * @param DateTimeImmutable $timestamp Comment timestamp
224     */
225    public function setTimestamp( DateTimeImmutable $timestamp ): void {
226        $this->timestamp = $timestamp;
227    }
228
229    /**
230     * @param string $author Comment author
231     */
232    public function setAuthor( string $author ): void {
233        $this->author = $author;
234    }
235}