Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.78% covered (success)
98.78%
81 / 82
93.33% covered (success)
93.33%
14 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommentFormatter
98.78% covered (success)
98.78%
81 / 82
93.33% covered (success)
93.33%
14 / 15
29
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
 createBatch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 format
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 formatBlock
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 formatLinksUnsafe
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 formatLinks
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 formatInternal
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 formatStrings
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 formatRevision
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 formatRevisions
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 createRevisionBatch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatItems
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatItemsInternal
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 wrapCommentWithBlock
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 preprocessRevComment
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\CommentFormatter;
4
5use MediaWiki\Linker\Linker;
6use MediaWiki\Linker\LinkTarget;
7use MediaWiki\Permissions\Authority;
8use MediaWiki\Revision\RevisionRecord;
9use Traversable;
10
11/**
12 * This is the main service interface for converting single-line comments from
13 * various DB comment fields into HTML.
14 *
15 * @since 1.38
16 */
17class CommentFormatter {
18    /** @var CommentParserFactory */
19    protected $parserFactory;
20
21    /**
22     * @internal Use MediaWikiServices::getCommentFormatter()
23     *
24     * @param CommentParserFactory $parserFactory
25     */
26    public function __construct( CommentParserFactory $parserFactory ) {
27        $this->parserFactory = $parserFactory;
28    }
29
30    /**
31     * Format comments using a fluent interface.
32     *
33     * @return CommentBatch
34     */
35    public function createBatch() {
36        return new CommentBatch( $this );
37    }
38
39    /**
40     * Format a single comment. Similar to the old Linker::formatComment().
41     *
42     * @param string $comment
43     * @param LinkTarget|null $selfLinkTarget The title used for fragment-only
44     *   and section links, formerly $title.
45     * @param bool $samePage If true, self links are rendered with a fragment-
46     *   only URL. Formerly $local.
47     * @param string|false|null $wikiId ID of the wiki to link to (if not the local
48     *   wiki), as used by WikiMap.
49     * @return string
50     */
51    public function format( string $comment, ?LinkTarget $selfLinkTarget = null,
52        $samePage = false, $wikiId = false
53    ) {
54        return $this->formatInternal( $comment, true, false, false,
55            $selfLinkTarget, $samePage, $wikiId );
56    }
57
58    /**
59     * Wrap a comment in standard punctuation and formatting if
60     * it's non-empty, otherwise return an empty string.
61     *
62     * @param string $comment
63     * @param LinkTarget|null $selfLinkTarget The title used for fragment-only
64     *   and section links, formerly $title.
65     * @param bool $samePage If true, self links are rendered with a fragment-
66     *   only URL. Formerly $local.
67     * @param string|false|null $wikiId ID of the wiki to link to (if not the local
68     *   wiki), as used by WikiMap.
69     * @param bool $useParentheses
70     * @return string
71     */
72    public function formatBlock( string $comment, ?LinkTarget $selfLinkTarget = null,
73        $samePage = false, $wikiId = false, $useParentheses = true
74    ) {
75        return $this->formatInternal( $comment, true, true, $useParentheses,
76            $selfLinkTarget, $samePage, $wikiId );
77    }
78
79    /**
80     * Format a comment, passing through HTML in the input to the output.
81     * This is unsafe and exists only for backwards compatibility with
82     * Linker::formatLinksInComment().
83     *
84     * In new code, use formatLinks() or createBatch()->disableSectionLinks().
85     *
86     * @internal
87     *
88     * @param string $comment
89     * @param LinkTarget|null $selfLinkTarget The title used for fragment-only
90     *   and section links, formerly $title.
91     * @param bool $samePage If true, self links are rendered with a fragment-
92     *   only URL. Formerly $local.
93     * @param string|false|null $wikiId ID of the wiki to link to (if not the local
94     *   wiki), as used by WikiMap.
95     * @return string
96     */
97    public function formatLinksUnsafe( string $comment, ?LinkTarget $selfLinkTarget = null,
98        $samePage = false, $wikiId = false
99    ) {
100        $parser = $this->parserFactory->create();
101        $preprocessed = $parser->preprocessUnsafe( $comment, $selfLinkTarget,
102            $samePage, $wikiId, false );
103        return $parser->finalize( $preprocessed );
104    }
105
106    /**
107     * Format links in a comment, ignoring section links in C-style comments.
108     *
109     * @param string $comment
110     * @param LinkTarget|null $selfLinkTarget The title used for fragment-only
111     *   and section links, formerly $title.
112     * @param bool $samePage If true, self links are rendered with a fragment-
113     *   only URL. Formerly $local.
114     * @param string|false|null $wikiId ID of the wiki to link to (if not the local
115     *   wiki), as used by WikiMap.
116     * @return string
117     */
118    public function formatLinks( string $comment, ?LinkTarget $selfLinkTarget = null,
119        $samePage = false, $wikiId = false
120    ) {
121        return $this->formatInternal( $comment, false, false, false,
122            $selfLinkTarget, $samePage, $wikiId );
123    }
124
125    /**
126     * Format a single comment with many ugly boolean parameters.
127     *
128     * @param string $comment
129     * @param bool $enableSectionLinks
130     * @param bool $useBlock
131     * @param bool $useParentheses
132     * @param LinkTarget|null $selfLinkTarget The title used for fragment-only
133     *   and section links, formerly $title.
134     * @param bool $samePage If true, self links are rendered with a fragment-
135     *   only URL. Formerly $local.
136     * @param string|false|null $wikiId ID of the wiki to link to (if not the local
137     *   wiki), as used by WikiMap.
138     * @return string|string[]
139     */
140    private function formatInternal( $comment, $enableSectionLinks, $useBlock, $useParentheses,
141        $selfLinkTarget = null, $samePage = false, $wikiId = false
142    ) {
143        $parser = $this->parserFactory->create();
144        $preprocessed = $parser->preprocess( $comment, $selfLinkTarget, $samePage, $wikiId,
145            $enableSectionLinks );
146        $output = $parser->finalize( $preprocessed );
147        if ( $useBlock ) {
148            $output = $this->wrapCommentWithBlock( $output, $useParentheses );
149        }
150        return $output;
151    }
152
153    /**
154     * Format comments which are provided as strings and all have the same
155     * self-link target and other options.
156     *
157     * If you need a different title for each comment, use createBatch().
158     *
159     * @param string[] $strings
160     * @param LinkTarget|null $selfLinkTarget The title used for fragment-only
161     *   and section links, formerly $title.
162     * @param bool $samePage If true, self links are rendered with a fragment-
163     *   only URL. Formerly $local.
164     * @param string|false|null $wikiId ID of the wiki to link to (if not the local
165     *   wiki), as used by WikiMap.
166     * @return string[]
167     */
168    public function formatStrings( $strings, ?LinkTarget $selfLinkTarget = null,
169        $samePage = false, $wikiId = false
170    ) {
171        $parser = $this->parserFactory->create();
172        $outputs = [];
173        foreach ( $strings as $i => $comment ) {
174            $outputs[$i] = $parser->preprocess( $comment, $selfLinkTarget, $samePage, $wikiId );
175        }
176        return $parser->finalize( $outputs );
177    }
178
179    /**
180     * Wrap and format the given revision's comment block, if the specified
181     * user is allowed to view it.
182     *
183     * This method produces HTML that requires CSS styles in mediawiki.interface.helpers.styles.
184     *
185     * NOTE: revision comments are special. This is not the same as getting a
186     * revision comment as a string and then formatting it with format().
187     *
188     * @param RevisionRecord $revision The revision to extract the comment and
189     *   title from. The title should always be populated, to avoid an additional
190     *   DB query.
191     * @param Authority $authority The user viewing the comment
192     * @param bool $samePage If true, self links are rendered with a fragment-
193     *   only URL. Formerly $local.
194     * @param bool $isPublic Show only if all users can see it
195     * @param bool $useParentheses Whether the comment is wrapped in parentheses
196     * @return string
197     */
198    public function formatRevision(
199        RevisionRecord $revision,
200        Authority $authority,
201        $samePage = false,
202        $isPublic = false,
203        $useParentheses = true
204    ) {
205        $parser = $this->parserFactory->create();
206        return $parser->finalize( $this->preprocessRevComment(
207            $parser, $authority, $revision, $samePage, $isPublic, $useParentheses ) );
208    }
209
210    /**
211     * Format multiple revision comments.
212     *
213     * @see CommentFormatter::formatRevision()
214     *
215     * @param iterable<RevisionRecord> $revisions
216     * @param Authority $authority
217     * @param bool $samePage
218     * @param bool $isPublic
219     * @param bool $useParentheses
220     * @param bool $indexById
221     * @return string|string[]
222     */
223    public function formatRevisions(
224        $revisions,
225        Authority $authority,
226        $samePage = false,
227        $isPublic = false,
228        $useParentheses = true,
229        $indexById = false
230    ) {
231        $parser = $this->parserFactory->create();
232        $outputs = [];
233        foreach ( $revisions as $i => $rev ) {
234            if ( $indexById ) {
235                $key = $rev->getId();
236            } else {
237                $key = $i;
238            }
239            // @phan-suppress-next-line PhanTypeMismatchDimAssignment getId does not return null here
240            $outputs[$key] = $this->preprocessRevComment(
241                $parser, $authority, $rev, $samePage, $isPublic, $useParentheses );
242        }
243        return $parser->finalize( $outputs );
244    }
245
246    /**
247     * Format a batch of revision comments using a fluent interface.
248     *
249     * @return RevisionCommentBatch
250     */
251    public function createRevisionBatch() {
252        return new RevisionCommentBatch( $this );
253    }
254
255    /**
256     * Format an iterator over CommentItem objects
257     *
258     * A shortcut for createBatch()->comments()->execute() for when you
259     * need to pass no other options.
260     *
261     * @param iterable<CommentItem>|Traversable $items
262     * @return string[]
263     */
264    public function formatItems( $items ) {
265        return $this->formatItemsInternal( $items );
266    }
267
268    /**
269     * @internal For use by CommentBatch
270     *
271     * Format comments with nullable batch options.
272     *
273     * @param iterable<CommentItem> $items
274     * @param LinkTarget|null $selfLinkTarget
275     * @param bool|null $samePage
276     * @param string|false|null $wikiId
277     * @param bool|null $enableSectionLinks
278     * @param bool|null $useBlock
279     * @param bool|null $useParentheses
280     * @return string[]
281     */
282    public function formatItemsInternal( $items, $selfLinkTarget = null,
283        $samePage = null, $wikiId = null, $enableSectionLinks = null,
284        $useBlock = null, $useParentheses = null
285    ) {
286        $outputs = [];
287        $parser = $this->parserFactory->create();
288        foreach ( $items as $index => $item ) {
289            $preprocessed = $parser->preprocess(
290                $item->comment,
291                $item->selfLinkTarget ?? $selfLinkTarget,
292                $item->samePage ?? $samePage ?? false,
293                $item->wikiId ?? $wikiId ?? false,
294                $enableSectionLinks ?? true
295            );
296            if ( $useBlock ?? false ) {
297                $preprocessed = $this->wrapCommentWithBlock(
298                    $preprocessed,
299                    $useParentheses ?? true
300                );
301            }
302            $outputs[$index] = $preprocessed;
303        }
304        return $parser->finalize( $outputs );
305    }
306
307    /**
308     * Wrap a comment in standard punctuation and formatting if
309     * it's non-empty, otherwise return empty string.
310     *
311     * @param string $formatted
312     * @param bool $useParentheses Whether the comment is wrapped in parentheses
313     *
314     * @return string
315     */
316    protected function wrapCommentWithBlock(
317        $formatted, $useParentheses
318    ) {
319        // '*' used to be the comment inserted by the software way back
320        // in antiquity in case none was provided, here for backwards
321        // compatibility, acc. to [brooke] -ævar
322        if ( $formatted == '' || $formatted == '*' ) {
323            return '';
324        }
325        if ( $useParentheses ) {
326            $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
327            $classNames = 'comment';
328        } else {
329            $classNames = 'comment comment--without-parentheses';
330        }
331        return " <span class=\"$classNames\">$formatted</span>";
332    }
333
334    /**
335     * Preprocess and wrap a revision comment.
336     *
337     * @param CommentParser $parser
338     * @param Authority $authority
339     * @param RevisionRecord $revRecord
340     * @param bool $samePage Whether section links should refer to local page
341     * @param bool $isPublic Show only if all users can see it
342     * @param bool $useParentheses (optional) Wrap comments in parentheses where needed
343     * @return string HTML fragment with link markers
344     */
345    private function preprocessRevComment(
346        CommentParser $parser,
347        Authority $authority,
348        RevisionRecord $revRecord,
349        $samePage = false,
350        $isPublic = false,
351        $useParentheses = true
352    ) {
353        if ( $revRecord->getComment( RevisionRecord::RAW ) === null ) {
354            return "";
355        }
356        if ( $revRecord->audienceCan(
357            RevisionRecord::DELETED_COMMENT,
358            $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
359            $authority )
360        ) {
361            $comment = $revRecord->getComment( RevisionRecord::FOR_THIS_USER, $authority );
362            $block = $parser->preprocess(
363                $comment ? $comment->text : '',
364                $revRecord->getPageAsLinkTarget(),
365                $samePage,
366                $revRecord->getWikiId(),
367                true
368            );
369            $block = $this->wrapCommentWithBlock( $block, $useParentheses );
370        } else {
371            $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
372        }
373        if ( $revRecord->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
374            $class = Linker::getRevisionDeletedClass( $revRecord );
375            return " <span class=\"$class comment\">$block</span>";
376        }
377        return $block;
378    }
379
380}