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