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