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