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