Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.94% covered (warning)
54.94%
228 / 415
7.89% covered (danger)
7.89%
3 / 38
CRAP
0.00% covered (danger)
0.00%
0 / 1
LogFormatter
55.07% covered (warning)
55.07%
228 / 414
7.89% covered (danger)
7.89%
3 / 38
2218.72
0.00% covered (danger)
0.00%
0 / 1
 newFromEntry
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 newFromRow
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setContext
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLinkRenderer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLinkRenderer
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setContentLanguage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContentLanguage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setCommentFormatter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCommentFormatter
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setUserEditTracker
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUserEditTracker
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setAudience
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 canViewLogType
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 canView
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 setShowUserToolLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPlainActionText
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getIRCActionComment
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getIRCActionText
87.27% covered (warning)
87.27%
144 / 165
0.00% covered (danger)
0.00%
0 / 1
43.30
 getActionText
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
7.73
 getActionMessage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getMessageKey
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getActionLinks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 extractParameters
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
90
 getMessageParameters
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 formatParameterValue
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
182
 makePageLink
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 getPerformerElement
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getComment
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getRestrictedElement
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 styleRestrictedElement
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 msg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeUserLink
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 getPreloadTitles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMessageParametersForTesting
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getParametersForApi
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatParametersForApi
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
4.16
 formatParameterValueForApi
97.78% covered (success)
97.78%
44 / 45
0.00% covered (danger)
0.00%
0 / 1
21
1<?php
2/**
3 * Contains a class for formatting log entries
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @author Niklas Laxström
22 * @license GPL-2.0-or-later
23 * @since 1.19
24 */
25
26namespace MediaWiki\Logging;
27
28use HtmlArmor;
29use MediaWiki\Api\ApiQueryBase;
30use MediaWiki\Api\ApiResult;
31use MediaWiki\CommentFormatter\CommentFormatter;
32use MediaWiki\Context\IContextSource;
33use MediaWiki\Context\RequestContext;
34use MediaWiki\Html\Html;
35use MediaWiki\Language\Language;
36use MediaWiki\Linker\Linker;
37use MediaWiki\Linker\LinkRenderer;
38use MediaWiki\Linker\LinkTarget;
39use MediaWiki\MainConfigNames;
40use MediaWiki\MediaWikiServices;
41use MediaWiki\Message\Message;
42use MediaWiki\SpecialPage\SpecialPage;
43use MediaWiki\Title\Title;
44use MediaWiki\User\User;
45use MediaWiki\User\UserEditTracker;
46use MediaWiki\User\UserIdentity;
47use stdClass;
48use Wikimedia\Message\MessageParam;
49use Wikimedia\Message\MessageSpecifier;
50
51/**
52 * Implements the default log formatting.
53 *
54 * Can be overridden by subclassing and setting:
55 * @code
56 *   $wgLogActionsHandlers['type/subtype'] = 'class'; or
57 *   $wgLogActionsHandlers['type/*'] = 'class';
58 * @endcode
59 *
60 * @stable to extend
61 * @since 1.19
62 */
63class LogFormatter {
64    // Audience options for viewing usernames, comments, and actions
65    public const FOR_PUBLIC = 1;
66    public const FOR_THIS_USER = 2;
67
68    // Static->
69
70    /**
71     * Constructs a new formatter suitable for given entry.
72     * @param LogEntry $entry
73     * @return LogFormatter
74     * @deprecated since 1.42, use LogFormatterFactory instead, hard-deprecated since 1.43
75     */
76    public static function newFromEntry( LogEntry $entry ) {
77        wfDeprecated( __METHOD__, '1.42' );
78        return MediaWikiServices::getInstance()->getLogFormatterFactory()->newFromEntry( $entry );
79    }
80
81    /**
82     * Handy shortcut for constructing a formatter directly from
83     * database row.
84     * @param stdClass|array $row
85     * @see DatabaseLogEntry::getSelectQueryData
86     * @return LogFormatter
87     * @deprecated since 1.42, use LogFormatterFactory instead, hard-deprecated since 1.43
88     */
89    public static function newFromRow( $row ) {
90        wfDeprecated( __METHOD__, '1.42' );
91        return self::newFromEntry( DatabaseLogEntry::newFromRow( $row ) );
92    }
93
94    // Nonstatic->
95
96    /** @var LogEntryBase */
97    protected $entry;
98
99    /** @var int Constant for handling log_deleted */
100    protected $audience = self::FOR_PUBLIC;
101
102    /** @var IContextSource Context for logging */
103    public $context;
104
105    /** @var bool Whether to output user tool links */
106    protected $linkFlood = false;
107
108    /**
109     * Set to true if we are constructing a message text that is going to
110     * be included in page history or send to IRC feed. Links are replaced
111     * with plaintext or with [[pagename]] kind of syntax, that is parsed
112     * by page histories and IRC feeds.
113     * @var bool
114     */
115    protected $plaintext = false;
116
117    /** @var bool */
118    protected $irctext = false;
119
120    /** @var LinkRenderer|null */
121    private $linkRenderer;
122
123    /** @var Language|null */
124    private $contentLanguage;
125
126    /** @var CommentFormatter|null */
127    private $commentFormatter;
128
129    /** @var UserEditTracker|null */
130    private $userEditTracker;
131
132    /**
133     * @see LogFormatter::getMessageParameters
134     * @var array|null
135     */
136    protected $parsedParameters;
137
138    /**
139     * @stable to call
140     *
141     * @param LogEntry $entry
142     */
143    public function __construct( LogEntry $entry ) {
144        $this->entry = $entry;
145        $this->context = RequestContext::getMain();
146    }
147
148    /**
149     * Replace the default context
150     */
151    public function setContext( IContextSource $context ) {
152        $this->context = $context;
153    }
154
155    /**
156     * @since 1.30
157     * @param LinkRenderer $linkRenderer
158     */
159    public function setLinkRenderer( LinkRenderer $linkRenderer ) {
160        $this->linkRenderer = $linkRenderer;
161    }
162
163    /**
164     * @since 1.30
165     * @return LinkRenderer
166     */
167    public function getLinkRenderer() {
168        if ( $this->linkRenderer !== null ) {
169            return $this->linkRenderer;
170        } else {
171            wfDeprecated( static::class . " without all required services", '1.42' );
172            return MediaWikiServices::getInstance()->getLinkRenderer();
173        }
174    }
175
176    /**
177     * @internal For factory only
178     * @since 1.42
179     * @param Language $contentLanguage
180     */
181    final public function setContentLanguage( Language $contentLanguage ) {
182        $this->contentLanguage = $contentLanguage;
183    }
184
185    /**
186     * @since 1.42
187     * @return Language
188     */
189    final public function getContentLanguage(): Language {
190        if ( $this->contentLanguage === null ) {
191            wfDeprecated( static::class . " without all required services", '1.42' );
192            $this->contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
193        }
194        return $this->contentLanguage;
195    }
196
197    /**
198     * @internal For factory only
199     * @since 1.42
200     * @param CommentFormatter $commentFormatter
201     */
202    final public function setCommentFormatter( CommentFormatter $commentFormatter ) {
203        $this->commentFormatter = $commentFormatter;
204    }
205
206    /**
207     * @since 1.42
208     * @return CommentFormatter
209     */
210    final public function getCommentFormatter(): CommentFormatter {
211        if ( $this->commentFormatter === null ) {
212            wfDeprecated( static::class . " without all required services", '1.42' );
213            $this->commentFormatter = MediaWikiServices::getInstance()->getCommentFormatter();
214        }
215        return $this->commentFormatter;
216    }
217
218    /**
219     * @internal For factory only
220     * @since 1.42
221     * @param UserEditTracker $userEditTracker
222     */
223    final public function setUserEditTracker( UserEditTracker $userEditTracker ) {
224        $this->userEditTracker = $userEditTracker;
225    }
226
227    /**
228     * @since 1.42
229     * @return UserEditTracker
230     */
231    final public function getUserEditTracker(): UserEditTracker {
232        if ( $this->userEditTracker === null ) {
233            wfDeprecated( static::class . " without all required services", '1.42' );
234            $this->userEditTracker = MediaWikiServices::getInstance()->getUserEditTracker();
235        }
236        return $this->userEditTracker;
237    }
238
239    /**
240     * Set the visibility restrictions for displaying content.
241     * If set to public, and an item is deleted, then it will be replaced
242     * with a placeholder even if the context user is allowed to view it.
243     * @param int $audience Const self::FOR_THIS_USER or self::FOR_PUBLIC
244     */
245    public function setAudience( $audience ) {
246        $this->audience = ( $audience == self::FOR_THIS_USER )
247            ? self::FOR_THIS_USER
248            : self::FOR_PUBLIC;
249    }
250
251    /**
252     * Check if a log item type can be displayed
253     * @return bool
254     */
255    public function canViewLogType() {
256        // If the user doesn't have the right permission to view the specific
257        // log type, return false
258        $logRestrictions = $this->context->getConfig()->get( MainConfigNames::LogRestrictions );
259        $type = $this->entry->getType();
260        return !isset( $logRestrictions[$type] )
261            || $this->context->getAuthority()->isAllowed( $logRestrictions[$type] );
262    }
263
264    /**
265     * Check if a log item can be displayed
266     * @param int $field LogPage::DELETED_* constant
267     * @return bool
268     */
269    protected function canView( $field ) {
270        if ( $this->audience == self::FOR_THIS_USER ) {
271            return LogEventsList::userCanBitfield(
272                $this->entry->getDeleted(), $field, $this->context->getAuthority() ) &&
273                self::canViewLogType();
274        } else {
275            return !$this->entry->isDeleted( $field ) && self::canViewLogType();
276        }
277    }
278
279    /**
280     * If set to true, will produce user tool links after
281     * the user name. This should be replaced with generic
282     * CSS/JS solution.
283     * @param bool $value
284     */
285    public function setShowUserToolLinks( $value ) {
286        $this->linkFlood = $value;
287    }
288
289    /**
290     * Ugly hack to produce plaintext version of the message.
291     * Usually you also want to set extraneous request context
292     * to avoid formatting for any particular user.
293     * @see getActionText()
294     * @return string Plain text
295     * @return-taint tainted
296     */
297    public function getPlainActionText() {
298        $this->plaintext = true;
299        $text = $this->getActionText();
300        $this->plaintext = false;
301
302        return $text;
303    }
304
305    /**
306     * Even uglier hack to maintain backwards compatibility with IRC bots
307     * (T36508).
308     * @see getActionText()
309     * @return string Text
310     */
311    public function getIRCActionComment() {
312        $actionComment = $this->getIRCActionText();
313        $comment = $this->entry->getComment();
314
315        if ( $comment != '' ) {
316            if ( $actionComment == '' ) {
317                $actionComment = $comment;
318            } else {
319                $actionComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
320            }
321        }
322
323        return $actionComment;
324    }
325
326    /**
327     * Even uglier hack to maintain backwards compatibility with IRC bots
328     * (T36508).
329     * @see getActionText()
330     * @return string Text
331     * @suppress SecurityCheck-XSS Working with plaintext
332     */
333    public function getIRCActionText() {
334        $this->plaintext = true;
335        $this->irctext = true;
336
337        $entry = $this->entry;
338        $parameters = $entry->getParameters();
339        // @see LogPage::actionText()
340        // Text of title the action is aimed at.
341        $target = $entry->getTarget()->getPrefixedText();
342        $text = null;
343        $contLang = $this->getContentLanguage();
344        switch ( $entry->getType() ) {
345            case 'move':
346                switch ( $entry->getSubtype() ) {
347                    case 'move':
348                        $movesource = $parameters['4::target'];
349                        $text = wfMessage( '1movedto2' )
350                            ->rawParams( $target, $movesource )->inContentLanguage()->escaped();
351                        break;
352                    case 'move_redir':
353                        $movesource = $parameters['4::target'];
354                        $text = wfMessage( '1movedto2_redir' )
355                            ->rawParams( $target, $movesource )->inContentLanguage()->escaped();
356                        break;
357                    case 'move-noredirect':
358                        break;
359                    case 'move_redir-noredirect':
360                        break;
361                }
362                break;
363
364            case 'delete':
365                switch ( $entry->getSubtype() ) {
366                    case 'delete':
367                        $text = wfMessage( 'deletedarticle' )
368                            ->rawParams( $target )->inContentLanguage()->escaped();
369                        break;
370                    case 'restore':
371                        $text = wfMessage( 'undeletedarticle' )
372                            ->rawParams( $target )->inContentLanguage()->escaped();
373                        break;
374                }
375                break;
376
377            case 'patrol':
378                // https://github.com/wikimedia/mediawiki/commit/1a05f8faf78675dc85984f27f355b8825b43efff
379                // Create a diff link to the patrolled revision
380                if ( $entry->getSubtype() === 'patrol' ) {
381                    $diffLink = htmlspecialchars(
382                        wfMessage( 'patrol-log-diff', $parameters['4::curid'] )
383                            ->inContentLanguage()->text() );
384                    $text = wfMessage( 'patrol-log-line', $diffLink, "[[$target]]", "" )
385                        ->inContentLanguage()->text();
386                } else {
387                    // broken??
388                }
389                break;
390
391            case 'protect':
392                switch ( $entry->getSubtype() ) {
393                    case 'protect':
394                        $text = wfMessage( 'protectedarticle' )
395                            ->rawParams( $target . ' ' . $parameters['4::description'] )
396                            ->inContentLanguage()
397                            ->escaped();
398                        break;
399                    case 'unprotect':
400                        $text = wfMessage( 'unprotectedarticle' )
401                            ->rawParams( $target )->inContentLanguage()->escaped();
402                        break;
403                    case 'modify':
404                        $text = wfMessage( 'modifiedarticleprotection' )
405                            ->rawParams( $target . ' ' . $parameters['4::description'] )
406                            ->inContentLanguage()
407                            ->escaped();
408                        break;
409                    case 'move_prot':
410                        $text = wfMessage( 'movedarticleprotection' )
411                            ->rawParams( $target, $parameters['4::oldtitle'] )->inContentLanguage()->escaped();
412                        break;
413                }
414                break;
415
416            case 'newusers':
417                switch ( $entry->getSubtype() ) {
418                    case 'newusers':
419                    case 'create':
420                        $text = wfMessage( 'newuserlog-create-entry' )
421                            ->inContentLanguage()->escaped();
422                        break;
423                    case 'create2':
424                    case 'byemail':
425                        $text = wfMessage( 'newuserlog-create2-entry' )
426                            ->rawParams( $target )->inContentLanguage()->escaped();
427                        break;
428                    case 'autocreate':
429                        $text = wfMessage( 'newuserlog-autocreate-entry' )
430                            ->inContentLanguage()->escaped();
431                        break;
432                }
433                break;
434
435            case 'upload':
436                switch ( $entry->getSubtype() ) {
437                    case 'upload':
438                        $text = wfMessage( 'uploadedimage' )
439                            ->rawParams( $target )->inContentLanguage()->escaped();
440                        break;
441                    case 'overwrite':
442                    case 'revert':
443                        $text = wfMessage( 'overwroteimage' )
444                            ->rawParams( $target )->inContentLanguage()->escaped();
445                        break;
446                }
447                break;
448
449            case 'rights':
450                if ( count( $parameters['4::oldgroups'] ) ) {
451                    $oldgroups = implode( ', ', $parameters['4::oldgroups'] );
452                } else {
453                    $oldgroups = wfMessage( 'rightsnone' )->inContentLanguage()->escaped();
454                }
455                if ( count( $parameters['5::newgroups'] ) ) {
456                    $newgroups = implode( ', ', $parameters['5::newgroups'] );
457                } else {
458                    $newgroups = wfMessage( 'rightsnone' )->inContentLanguage()->escaped();
459                }
460                switch ( $entry->getSubtype() ) {
461                    case 'rights':
462                        $text = wfMessage( 'rightslogentry' )
463                            ->rawParams( $target, $oldgroups, $newgroups )->inContentLanguage()->escaped();
464                        break;
465                    case 'autopromote':
466                        $text = wfMessage( 'rightslogentry-autopromote' )
467                            ->rawParams( $target, $oldgroups, $newgroups )->inContentLanguage()->escaped();
468                        break;
469                }
470                break;
471
472            case 'merge':
473                $text = wfMessage( 'pagemerge-logentry' )
474                    ->rawParams( $target, $parameters['4::dest'], $parameters['5::mergepoint'] )
475                    ->inContentLanguage()->escaped();
476                break;
477
478            case 'block':
479                switch ( $entry->getSubtype() ) {
480                    case 'block':
481                        // Keep compatibility with extensions by checking for
482                        // new key (5::duration/6::flags) or old key (0/optional 1)
483                        if ( $entry->isLegacy() ) {
484                            $rawDuration = $parameters[0];
485                            $rawFlags = $parameters[1] ?? '';
486                        } else {
487                            $rawDuration = $parameters['5::duration'];
488                            $rawFlags = $parameters['6::flags'];
489                        }
490                        $duration = $contLang->translateBlockExpiry(
491                            $rawDuration,
492                            null,
493                            (int)wfTimestamp( TS_UNIX, $entry->getTimestamp() )
494                        );
495                        $flags = BlockLogFormatter::formatBlockFlags( $rawFlags, $contLang );
496                        $text = wfMessage( 'blocklogentry' )
497                            ->rawParams( $target, $duration, $flags )->inContentLanguage()->escaped();
498                        break;
499                    case 'unblock':
500                        $text = wfMessage( 'unblocklogentry' )
501                            ->rawParams( $target )->inContentLanguage()->escaped();
502                        break;
503                    case 'reblock':
504                        $duration = $contLang->translateBlockExpiry(
505                            $parameters['5::duration'],
506                            null,
507                            (int)wfTimestamp( TS_UNIX, $entry->getTimestamp() )
508                        );
509                        $flags = BlockLogFormatter::formatBlockFlags( $parameters['6::flags'],
510                            $contLang );
511                        $text = wfMessage( 'reblock-logentry' )
512                            ->rawParams( $target, $duration, $flags )->inContentLanguage()->escaped();
513                        break;
514                }
515                break;
516
517            case 'import':
518                switch ( $entry->getSubtype() ) {
519                    case 'upload':
520                        $text = wfMessage( 'import-logentry-upload' )
521                            ->rawParams( $target )->inContentLanguage()->escaped();
522                        break;
523                    case 'interwiki':
524                        $text = wfMessage( 'import-logentry-interwiki' )
525                            ->rawParams( $target )->inContentLanguage()->escaped();
526                        break;
527                }
528                break;
529            // case 'suppress' --private log -- aaron  (so we know who to blame in a few years :-D)
530            // default:
531        }
532
533        $this->plaintext = false;
534        $this->irctext = false;
535
536        return $text ?? $this->getPlainActionText();
537    }
538
539    /**
540     * Gets the log action, including username.
541     * @stable to override
542     * @return string HTML
543     * phan-taint-check gets very confused by $this->plaintext, so disable.
544     * @return-taint onlysafefor_html
545     */
546    public function getActionText() {
547        if ( $this->canView( LogPage::DELETED_ACTION ) ) {
548            $element = $this->getActionMessage();
549            if ( $element instanceof Message ) {
550                $element = $this->plaintext ? $element->text() : $element->escaped();
551            }
552            if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) {
553                $element = $this->styleRestrictedElement( $element );
554            }
555        } else {
556            $sep = $this->msg( 'word-separator' );
557            $sep = $this->plaintext ? $sep->text() : $sep->escaped();
558            $performer = $this->getPerformerElement();
559            $element = $performer . $sep . $this->getRestrictedElement( 'rev-deleted-event' );
560        }
561
562        return $element;
563    }
564
565    /**
566     * Returns a sentence describing the log action. Usually
567     * a Message object is returned, but old style log types
568     * and entries might return pre-escaped HTML string.
569     * @return Message|string Pre-escaped HTML
570     */
571    protected function getActionMessage() {
572        $message = $this->msg( $this->getMessageKey() );
573        $message->params( $this->getMessageParameters() );
574
575        return $message;
576    }
577
578    /**
579     * Returns a key to be used for formatting the action sentence.
580     * Default is logentry-TYPE-SUBTYPE for modern logs. Legacy log
581     * types will use custom keys, and subclasses can also alter the
582     * key depending on the entry itself.
583     * @stable to override
584     * @return string Message key
585     */
586    protected function getMessageKey() {
587        $type = $this->entry->getType();
588        $subtype = $this->entry->getSubtype();
589
590        return "logentry-$type-$subtype";
591    }
592
593    /**
594     * Returns extra links that comes after the action text, like "revert", etc.
595     *
596     * @stable to override
597     * @return string
598     */
599    public function getActionLinks() {
600        return '';
601    }
602
603    /**
604     * Extracts the optional extra parameters for use in action messages.
605     * The array indexes start from number 3.
606     * @stable to override
607     * @return array
608     */
609    protected function extractParameters() {
610        $entry = $this->entry;
611        $params = [];
612
613        if ( $entry->isLegacy() ) {
614            foreach ( $entry->getParameters() as $index => $value ) {
615                $params[$index + 3] = $value;
616            }
617        }
618
619        // Filter out parameters which are not in format #:foo
620        foreach ( $entry->getParameters() as $key => $value ) {
621            if ( strpos( $key, ':' ) === false ) {
622                continue;
623            }
624            [ $index, $type, ] = explode( ':', $key, 3 );
625            if ( ctype_digit( $index ) ) {
626                $params[(int)$index - 1] = $this->formatParameterValue( $type, $value );
627            }
628        }
629
630        /* Message class doesn't like non consecutive numbering.
631         * Fill in missing indexes with empty strings to avoid
632         * incorrect renumbering.
633         */
634        if ( count( $params ) ) {
635            $max = max( array_keys( $params ) );
636            // index 0 to 2 are added in getMessageParameters
637            for ( $i = 3; $i < $max; $i++ ) {
638                if ( !isset( $params[$i] ) ) {
639                    $params[$i] = '';
640                }
641            }
642        }
643
644        return $params;
645    }
646
647    /**
648     * Formats parameters intended for action message from array of all parameters.
649     * There are three hardcoded parameters:
650     *  - $1: user name with premade link
651     *  - $2: usable for gender magic function
652     *  - $3: target page with premade link
653     * More parameters might be present, depending on what code created the log
654     * entry.
655     *
656     * The parameters are returned as a non-associative array that can be passed to
657     * Message::params(), so $logFormatter->getMessageParameters()[0] is the $1 parameter
658     * in the message and so on.
659     *
660     * @stable to override
661     * @return array
662     * @see ManualLogEntry::setParameters() for how parameters are determined.
663     */
664    protected function getMessageParameters() {
665        if ( $this->parsedParameters !== null ) {
666            return $this->parsedParameters;
667        }
668
669        $entry = $this->entry;
670        $params = $this->extractParameters();
671        $params[0] = Message::rawParam( $this->getPerformerElement() );
672        $params[1] = $this->canView( LogPage::DELETED_USER ) ? $entry->getPerformerIdentity()->getName() : '';
673        $params[2] = Message::rawParam( $this->makePageLink( $entry->getTarget() ) );
674
675        // Bad things happens if the numbers are not in correct order
676        ksort( $params );
677
678        $this->parsedParameters = $params;
679        return $this->parsedParameters;
680    }
681
682    /**
683     * Formats parameters values dependent to their type
684     * @param string $type The type of the value.
685     *   Valid are currently:
686     *     * - (empty) or plain: The value is returned as-is
687     *     * raw: The value will be added to the log message
688     *            as raw parameter (e.g. no escaping)
689     *            Use this only if there is no other working
690     *            type like user-link or title-link
691     *     * msg: The value is a message-key, the output is
692     *            the message in user language
693     *     * msg-content: The value is a message-key, the output
694     *                    is the message in content language
695     *     * user: The value is a user name, e.g. for GENDER
696     *     * user-link: The value is a user name, returns a
697     *                  link for the user
698     *     * title: The value is a page title,
699     *              returns name of page
700     *     * title-link: The value is a page title,
701     *                   returns link to this page
702     *     * number: Format value as number
703     *     * list: Format value as a comma-separated list
704     * @param mixed $value The parameter value that should be formatted
705     * @return mixed Formatted value
706     * @since 1.21
707     */
708    protected function formatParameterValue( $type, $value ) {
709        $saveLinkFlood = $this->linkFlood;
710
711        switch ( strtolower( trim( $type ) ) ) {
712            case 'raw':
713                $value = Message::rawParam( $value );
714                break;
715            case 'list':
716                $value = $this->context->getLanguage()->commaList( $value );
717                break;
718            case 'msg':
719                $value = $this->msg( $value )->text();
720                break;
721            case 'msg-content':
722                $value = $this->msg( $value )->inContentLanguage()->text();
723                break;
724            case 'number':
725                $value = Message::numParam( $value );
726                break;
727            case 'user':
728                $user = User::newFromName( $value );
729                $value = $user->getName();
730                break;
731            case 'user-link':
732                $this->setShowUserToolLinks( false );
733
734                $user = User::newFromName( $value );
735
736                if ( !$user ) {
737                    $value = $this->msg( 'empty-username' )->text();
738                } else {
739                    $value = Message::rawParam( $this->makeUserLink( $user ) );
740                    $this->setShowUserToolLinks( $saveLinkFlood );
741                }
742                break;
743            case 'title':
744                $title = Title::newFromText( $value );
745                $value = $title->getPrefixedText();
746                break;
747            case 'title-link':
748                $title = Title::newFromText( $value );
749                $value = Message::rawParam( $this->makePageLink( $title ) );
750                break;
751            case 'plain':
752                // Plain text, nothing to do
753            default:
754                // Catch other types and use the old behavior (return as-is)
755        }
756
757        return $value;
758    }
759
760    /**
761     * Helper to make a link to the page, taking the plaintext
762     * value in consideration.
763     * @stable to override
764     * @param Title|null $title The page
765     * @param array $parameters Query parameters
766     * @param string|null $html Linktext of the link as raw html
767     * @return string wikitext or html
768     * @return-taint onlysafefor_html
769     */
770    protected function makePageLink( ?Title $title = null, $parameters = [], $html = null ) {
771        if ( !$title instanceof Title ) {
772            $msg = $this->msg( 'invalidtitle' )->text();
773            if ( $this->plaintext ) {
774                return $msg;
775            } else {
776                return Html::element( 'span', [ 'class' => 'mw-invalidtitle' ], $msg );
777            }
778        }
779
780        if ( $this->plaintext ) {
781            $link = '[[' . $title->getPrefixedText() . ']]';
782        } else {
783            $html = $html !== null ? new HtmlArmor( $html ) : $html;
784            $link = $this->getLinkRenderer()->makeLink( $title, $html, [], $parameters );
785        }
786
787        return $link;
788    }
789
790    /**
791     * Provides the name of the user who performed the log action.
792     * Used as part of log action message or standalone, depending
793     * which parts of the log entry has been hidden.
794     * @return string
795     */
796    public function getPerformerElement() {
797        if ( $this->canView( LogPage::DELETED_USER ) ) {
798            $performerIdentity = $this->entry->getPerformerIdentity();
799            $element = $this->makeUserLink( $performerIdentity );
800            if ( $this->entry->isDeleted( LogPage::DELETED_USER ) ) {
801                $element = $this->styleRestrictedElement( $element );
802            }
803        } else {
804            $element = $this->getRestrictedElement( 'rev-deleted-user' );
805        }
806
807        return $element;
808    }
809
810    /**
811     * Gets the user provided comment
812     * @stable to override
813     * @return string HTML
814     */
815    public function getComment() {
816        if ( $this->canView( LogPage::DELETED_COMMENT ) ) {
817            $comment = $this->getCommentFormatter()
818                ->formatBlock( $this->entry->getComment() );
819            // No hard coded spaces thanx
820            $element = ltrim( $comment );
821            if ( $this->entry->isDeleted( LogPage::DELETED_COMMENT ) ) {
822                $element = $this->styleRestrictedElement( $element );
823            }
824        } else {
825            $element = $this->getRestrictedElement( 'rev-deleted-comment' );
826        }
827
828        return $element;
829    }
830
831    /**
832     * Helper method for displaying restricted element.
833     * @param string $message
834     * @return string HTML or wiki text
835     * @return-taint onlysafefor_html
836     */
837    protected function getRestrictedElement( $message ) {
838        if ( $this->plaintext ) {
839            return $this->msg( $message )->text();
840        }
841
842        return $this->styleRestrictedElement( $this->msg( $message )->escaped() );
843    }
844
845    /**
846     * Helper method for styling restricted element.
847     * @param string $content
848     * @return string HTML or wiki text
849     */
850    protected function styleRestrictedElement( $content ) {
851        if ( $this->plaintext ) {
852            return $content;
853        }
854        $attribs = [ 'class' => [ 'history-deleted' ] ];
855        if ( $this->entry->isDeleted( LogPage::DELETED_RESTRICTED ) ) {
856            $attribs['class'][] = 'mw-history-suppressed';
857        }
858
859        return Html::rawElement( 'span', $attribs, $content );
860    }
861
862    /**
863     * Shortcut for wfMessage which honors local context.
864     * @param string $key
865     * @phpcs:ignore Generic.Files.LineLength
866     * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$params
867     *   See Message::params()
868     * @return Message
869     */
870    protected function msg( $key, ...$params ) {
871        return $this->context->msg( $key, ...$params );
872    }
873
874    /**
875     * @param UserIdentity $user
876     * @param int $toolFlags Combination of Linker::TOOL_LINKS_* flags
877     * @return string wikitext or html
878     * @return-taint onlysafefor_html
879     */
880    protected function makeUserLink( UserIdentity $user, $toolFlags = 0 ) {
881        if ( $this->plaintext ) {
882            $element = $user->getName();
883        } else {
884            $element = Linker::userLink(
885                $user->getId(),
886                $user->getName()
887            );
888            if ( $this->linkFlood ) {
889                $editCount = $this->getUserEditTracker()->getUserEditCount( $user );
890
891                $element .= Linker::userToolLinks(
892                    $user->getId(),
893                    $user->getName(),
894                    true, // redContribsWhenNoEdits
895                    $toolFlags,
896                    $editCount,
897                    // do not render parentheses in the HTML markup (CSS will provide)
898                    false
899                );
900            }
901        }
902
903        return $element;
904    }
905
906    /**
907     * @stable to override
908     * @return LinkTarget[] Array of titles that should be preloaded with LinkBatch
909     */
910    public function getPreloadTitles() {
911        return [];
912    }
913
914    /**
915     * @return array Output of getMessageParameters() for testing
916     */
917    public function getMessageParametersForTesting() {
918        // This function was added because getMessageParameters() is
919        // protected and a change from protected to public caused
920        // problems with extensions
921        return $this->getMessageParameters();
922    }
923
924    /**
925     * Get the array of parameters, converted from legacy format if necessary.
926     * @since 1.25
927     * @stable to override
928     * @return array
929     */
930    protected function getParametersForApi() {
931        return $this->entry->getParameters();
932    }
933
934    /**
935     * Format parameters for API output
936     *
937     * The result array should generally map named keys to values. Index and
938     * type should be omitted, e.g. "4::foo" should be returned as "foo" in the
939     * output. Values should generally be unformatted.
940     *
941     * Renames or removals of keys besides from the legacy numeric format to
942     * modern named style should be avoided. Any renames should be announced to
943     * the mediawiki-api-announce mailing list.
944     *
945     * @since 1.25
946     * @stable to override
947     * @return array
948     */
949    public function formatParametersForApi() {
950        $logParams = [];
951        foreach ( $this->getParametersForApi() as $key => $value ) {
952            $vals = explode( ':', $key, 3 );
953            if ( count( $vals ) !== 3 ) {
954                if ( $value instanceof \__PHP_Incomplete_Class ) {
955                    wfLogWarning( 'Log entry of type ' . $this->entry->getFullType() .
956                        ' contains unrecoverable extra parameters.' );
957                    continue;
958                }
959                $logParams[$key] = $value;
960                continue;
961            }
962            $logParams += $this->formatParameterValueForApi( $vals[2], $vals[1], $value );
963        }
964        ApiResult::setIndexedTagName( $logParams, 'param' );
965        ApiResult::setArrayType( $logParams, 'assoc' );
966
967        return $logParams;
968    }
969
970    /**
971     * Format a single parameter value for API output
972     *
973     * @since 1.25
974     * @param string $name
975     * @param string $type
976     * @param string $value
977     * @return array
978     */
979    protected function formatParameterValueForApi( $name, $type, $value ) {
980        $type = strtolower( trim( $type ) );
981        switch ( $type ) {
982            case 'bool':
983                $value = (bool)$value;
984                break;
985
986            case 'number':
987                if ( is_int( $value ) || ctype_digit( (string)$value ) ) {
988                    $value = (int)$value;
989                } else {
990                    $value = (float)$value;
991                }
992                break;
993
994            case 'array':
995            case 'assoc':
996            case 'kvp':
997                if ( is_array( $value ) ) {
998                    ApiResult::setArrayType( $value, $type );
999                }
1000                break;
1001
1002            case 'timestamp':
1003                $value = wfTimestamp( TS_ISO_8601, $value );
1004                break;
1005
1006            case 'msg':
1007            case 'msg-content':
1008                $msg = $this->msg( $value );
1009                if ( $type === 'msg-content' ) {
1010                    $msg->inContentLanguage();
1011                }
1012                $value = [];
1013                $value["{$name}_key"] = $msg->getKey();
1014                if ( $msg->getParams() ) {
1015                    $value["{$name}_params"] = $msg->getParams();
1016                }
1017                $value["{$name}_text"] = $msg->text();
1018                return $value;
1019
1020            case 'title':
1021            case 'title-link':
1022                $title = Title::newFromText( $value );
1023                if ( !$title ) {
1024                    $title = SpecialPage::getTitleFor( 'Badtitle', $value );
1025                }
1026                $value = [];
1027                ApiQueryBase::addTitleInfo( $value, $title, "{$name}_" );
1028                return $value;
1029
1030            case 'user':
1031            case 'user-link':
1032                $user = User::newFromName( $value );
1033                if ( $user ) {
1034                    $value = $user->getName();
1035                }
1036                break;
1037
1038            default:
1039                // do nothing
1040                break;
1041        }
1042
1043        return [ $name => $value ];
1044    }
1045}
1046
1047/** @deprecated class alias since 1.44 */
1048class_alias( LogFormatter::class, 'LogFormatter' );