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