Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.75% covered (success)
97.75%
87 / 89
87.50% covered (warning)
87.50%
14 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommentFormatter
97.75% covered (success)
97.75%
87 / 89
87.50% covered (warning)
87.50%
14 / 16
31
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
 formatStringsAsBlock
100.00% covered (success)
100.00%
7 / 7
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
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 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     * Given an array of comments as strings which all have the same self link
181     * target, format the comments and wrap them in standard punctuation and
182     * formatting.
183     *
184     * If you need a different title for each comment, use createBatch().
185     *
186     * @param string[] $strings
187     * @param LinkTarget|null $selfLinkTarget The title used for fragment-only
188     *   and section links, formerly $title.
189     * @param bool $samePage If true, self links are rendered with a fragment-
190     *   only URL. Formerly $local.
191     * @param string|false|null $wikiId ID of the wiki to link to (if not the local
192     *   wiki), as used by WikiMap.
193     * @param bool $useParentheses
194     * @return string[]
195     */
196    public function formatStringsAsBlock( $strings, LinkTarget $selfLinkTarget = null,
197        $samePage = false, $wikiId = false, $useParentheses = true
198    ) {
199        $parser = $this->parserFactory->create();
200        $outputs = [];
201        foreach ( $strings as $i => $comment ) {
202            $outputs[$i] = $this->wrapCommentWithBlock(
203                $parser->preprocess( $comment, $selfLinkTarget, $samePage, $wikiId ),
204                $useParentheses );
205        }
206        return $parser->finalize( $outputs );
207    }
208
209    /**
210     * Wrap and format the given revision's comment block, if the specified
211     * user is allowed to view it.
212     *
213     * This method produces HTML that requires CSS styles in mediawiki.interface.helpers.styles.
214     *
215     * NOTE: revision comments are special. This is not the same as getting a
216     * revision comment as a string and then formatting it with format().
217     *
218     * @param RevisionRecord $revision The revision to extract the comment and
219     *   title from. The title should always be populated, to avoid an additional
220     *   DB query.
221     * @param Authority $authority The user viewing the comment
222     * @param bool $samePage If true, self links are rendered with a fragment-
223     *   only URL. Formerly $local.
224     * @param bool $isPublic Show only if all users can see it
225     * @param bool $useParentheses Whether the comment is wrapped in parentheses
226     * @return string
227     */
228    public function formatRevision(
229        RevisionRecord $revision,
230        Authority $authority,
231        $samePage = false,
232        $isPublic = false,
233        $useParentheses = true
234    ) {
235        $parser = $this->parserFactory->create();
236        return $parser->finalize( $this->preprocessRevComment(
237            $parser, $authority, $revision, $samePage, $isPublic, $useParentheses ) );
238    }
239
240    /**
241     * Format multiple revision comments.
242     *
243     * @see CommentFormatter::formatRevision()
244     *
245     * @param iterable<RevisionRecord> $revisions
246     * @param Authority $authority
247     * @param bool $samePage
248     * @param bool $isPublic
249     * @param bool $useParentheses
250     * @param bool $indexById
251     * @return string|string[]
252     */
253    public function formatRevisions(
254        $revisions,
255        Authority $authority,
256        $samePage = false,
257        $isPublic = false,
258        $useParentheses = true,
259        $indexById = false
260    ) {
261        $parser = $this->parserFactory->create();
262        $outputs = [];
263        foreach ( $revisions as $i => $rev ) {
264            if ( $indexById ) {
265                $key = $rev->getId();
266            } else {
267                $key = $i;
268            }
269            // @phan-suppress-next-line PhanTypeMismatchDimAssignment getId does not return null here
270            $outputs[$key] = $this->preprocessRevComment(
271                $parser, $authority, $rev, $samePage, $isPublic, $useParentheses );
272        }
273        return $parser->finalize( $outputs );
274    }
275
276    /**
277     * Format a batch of revision comments using a fluent interface.
278     *
279     * @return RevisionCommentBatch
280     */
281    public function createRevisionBatch() {
282        return new RevisionCommentBatch( $this );
283    }
284
285    /**
286     * Format an iterator over CommentItem objects
287     *
288     * A shortcut for createBatch()->comments()->execute() for when you
289     * need to pass no other options.
290     *
291     * @param iterable<CommentItem>|Traversable $items
292     * @return string[]
293     */
294    public function formatItems( $items ) {
295        return $this->formatItemsInternal( $items );
296    }
297
298    /**
299     * @internal For use by CommentBatch
300     *
301     * Format comments with nullable batch options.
302     *
303     * @param iterable<CommentItem> $items
304     * @param LinkTarget|null $selfLinkTarget
305     * @param bool|null $samePage
306     * @param string|false|null $wikiId
307     * @param bool|null $enableSectionLinks
308     * @param bool|null $useBlock
309     * @param bool|null $useParentheses
310     * @return string[]
311     */
312    public function formatItemsInternal( $items, $selfLinkTarget = null,
313        $samePage = null, $wikiId = null, $enableSectionLinks = null,
314        $useBlock = null, $useParentheses = null
315    ) {
316        $outputs = [];
317        $parser = $this->parserFactory->create();
318        foreach ( $items as $index => $item ) {
319            $preprocessed = $parser->preprocess(
320                $item->comment,
321                $item->selfLinkTarget ?? $selfLinkTarget,
322                $item->samePage ?? $samePage ?? false,
323                $item->wikiId ?? $wikiId ?? false,
324                $enableSectionLinks ?? true
325            );
326            if ( $useBlock ?? false ) {
327                $preprocessed = $this->wrapCommentWithBlock(
328                    $preprocessed,
329                    $useParentheses ?? true
330                );
331            }
332            $outputs[$index] = $preprocessed;
333        }
334        return $parser->finalize( $outputs );
335    }
336
337    /**
338     * Wrap a comment in standard punctuation and formatting if
339     * it's non-empty, otherwise return empty string.
340     *
341     * @param string $formatted
342     * @param bool $useParentheses Whether the comment is wrapped in parentheses
343     *
344     * @return string
345     */
346    protected function wrapCommentWithBlock(
347        $formatted, $useParentheses
348    ) {
349        // '*' used to be the comment inserted by the software way back
350        // in antiquity in case none was provided, here for backwards
351        // compatibility, acc. to [brooke] -ævar
352        if ( $formatted == '' || $formatted == '*' ) {
353            return '';
354        }
355        if ( $useParentheses ) {
356            $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
357            $classNames = 'comment';
358        } else {
359            $classNames = 'comment comment--without-parentheses';
360        }
361        return " <span class=\"$classNames\">$formatted</span>";
362    }
363
364    /**
365     * Preprocess and wrap a revision comment.
366     *
367     * @param CommentParser $parser
368     * @param Authority $authority
369     * @param RevisionRecord $revRecord
370     * @param bool $samePage Whether section links should refer to local page
371     * @param bool $isPublic Show only if all users can see it
372     * @param bool $useParentheses (optional) Wrap comments in parentheses where needed
373     * @return string HTML fragment with link markers
374     */
375    private function preprocessRevComment(
376        CommentParser $parser,
377        Authority $authority,
378        RevisionRecord $revRecord,
379        $samePage = false,
380        $isPublic = false,
381        $useParentheses = true
382    ) {
383        if ( $revRecord->getComment( RevisionRecord::RAW ) === null ) {
384            return "";
385        }
386        if ( $revRecord->audienceCan(
387            RevisionRecord::DELETED_COMMENT,
388            $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
389            $authority )
390        ) {
391            $comment = $revRecord->getComment( RevisionRecord::FOR_THIS_USER, $authority );
392            $block = $parser->preprocess(
393                $comment ? $comment->text : '',
394                $revRecord->getPageAsLinkTarget(),
395                $samePage,
396                null,
397                true
398            );
399            $block = $this->wrapCommentWithBlock( $block, $useParentheses );
400        } else {
401            $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
402        }
403        if ( $revRecord->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
404            $class = Linker::getRevisionDeletedClass( $revRecord );
405            return " <span class=\"$class comment\">$block</span>";
406        }
407        return $block;
408    }
409
410}