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