Completed
Branch master (939199)
by
unknown
39:35
created

includes/logging/LogFormatter.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
26
/**
27
 * Implements the default log formatting.
28
 *
29
 * Can be overridden by subclassing and setting:
30
 *
31
 *     $wgLogActionsHandlers['type/subtype'] = 'class'; or
32
 *     $wgLogActionsHandlers['type/*'] = 'class';
33
 *
34
 * @since 1.19
35
 */
36
class LogFormatter {
37
	// Audience options for viewing usernames, comments, and actions
38
	const FOR_PUBLIC = 1;
39
	const FOR_THIS_USER = 2;
40
41
	// Static->
42
43
	/**
44
	 * Constructs a new formatter suitable for given entry.
45
	 * @param LogEntry $entry
46
	 * @return LogFormatter
47
	 */
48
	public static function newFromEntry( LogEntry $entry ) {
49
		global $wgLogActionsHandlers;
50
		$fulltype = $entry->getFullType();
51
		$wildcard = $entry->getType() . '/*';
52
		$handler = '';
53
54
		if ( isset( $wgLogActionsHandlers[$fulltype] ) ) {
55
			$handler = $wgLogActionsHandlers[$fulltype];
56
		} elseif ( isset( $wgLogActionsHandlers[$wildcard] ) ) {
57
			$handler = $wgLogActionsHandlers[$wildcard];
58
		}
59
60
		if ( $handler !== '' && is_string( $handler ) && class_exists( $handler ) ) {
61
			return new $handler( $entry );
62
		}
63
64
		return new LegacyLogFormatter( $entry );
65
	}
66
67
	/**
68
	 * Handy shortcut for constructing a formatter directly from
69
	 * database row.
70
	 * @param stdClass|array $row
71
	 * @see DatabaseLogEntry::getSelectQueryData
72
	 * @return LogFormatter
73
	 */
74
	public static function newFromRow( $row ) {
75
		return self::newFromEntry( DatabaseLogEntry::newFromRow( $row ) );
76
	}
77
78
	// Nonstatic->
79
80
	/** @var LogEntryBase */
81
	protected $entry;
82
83
	/** @var int Constant for handling log_deleted */
84
	protected $audience = self::FOR_PUBLIC;
85
86
	/** @var IContextSource Context for logging */
87
	public $context;
88
89
	/** @var bool Whether to output user tool links */
90
	protected $linkFlood = false;
91
92
	/**
93
	 * Set to true if we are constructing a message text that is going to
94
	 * be included in page history or send to IRC feed. Links are replaced
95
	 * with plaintext or with [[pagename]] kind of syntax, that is parsed
96
	 * by page histories and IRC feeds.
97
	 * @var string
98
	 */
99
	protected $plaintext = false;
100
101
	/** @var string */
102
	protected $irctext = false;
103
104
	protected function __construct( LogEntry $entry ) {
105
		$this->entry = $entry;
106
		$this->context = RequestContext::getMain();
107
	}
108
109
	/**
110
	 * Replace the default context
111
	 * @param IContextSource $context
112
	 */
113
	public function setContext( IContextSource $context ) {
114
		$this->context = $context;
115
	}
116
117
	/**
118
	 * Set the visibility restrictions for displaying content.
119
	 * If set to public, and an item is deleted, then it will be replaced
120
	 * with a placeholder even if the context user is allowed to view it.
121
	 * @param int $audience Const self::FOR_THIS_USER or self::FOR_PUBLIC
122
	 */
123
	public function setAudience( $audience ) {
124
		$this->audience = ( $audience == self::FOR_THIS_USER )
125
			? self::FOR_THIS_USER
126
			: self::FOR_PUBLIC;
127
	}
128
129
	/**
130
	 * Check if a log item can be displayed
131
	 * @param int $field LogPage::DELETED_* constant
132
	 * @return bool
133
	 */
134
	protected function canView( $field ) {
135
		if ( $this->audience == self::FOR_THIS_USER ) {
136
			return LogEventsList::userCanBitfield(
137
				$this->entry->getDeleted(), $field, $this->context->getUser() );
138
		} else {
139
			return !$this->entry->isDeleted( $field );
140
		}
141
	}
142
143
	/**
144
	 * If set to true, will produce user tool links after
145
	 * the user name. This should be replaced with generic
146
	 * CSS/JS solution.
147
	 * @param bool $value
148
	 */
149
	public function setShowUserToolLinks( $value ) {
150
		$this->linkFlood = $value;
151
	}
152
153
	/**
154
	 * Ugly hack to produce plaintext version of the message.
155
	 * Usually you also want to set extraneous request context
156
	 * to avoid formatting for any particular user.
157
	 * @see getActionText()
158
	 * @return string Plain text
159
	 */
160
	public function getPlainActionText() {
161
		$this->plaintext = true;
162
		$text = $this->getActionText();
163
		$this->plaintext = false;
164
165
		return $text;
166
	}
167
168
	/**
169
	 * Even uglier hack to maintain backwards compatibilty with IRC bots
170
	 * (bug 34508).
171
	 * @see getActionText()
172
	 * @return string Text
173
	 */
174
	public function getIRCActionComment() {
175
		$actionComment = $this->getIRCActionText();
176
		$comment = $this->entry->getComment();
177
178
		if ( $comment != '' ) {
179
			if ( $actionComment == '' ) {
180
				$actionComment = $comment;
181
			} else {
182
				$actionComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
183
			}
184
		}
185
186
		return $actionComment;
187
	}
188
189
	/**
190
	 * Even uglier hack to maintain backwards compatibilty with IRC bots
191
	 * (bug 34508).
192
	 * @see getActionText()
193
	 * @return string Text
194
	 */
195
	public function getIRCActionText() {
196
		global $wgContLang;
197
198
		$this->plaintext = true;
199
		$this->irctext = true;
200
201
		$entry = $this->entry;
202
		$parameters = $entry->getParameters();
203
		// @see LogPage::actionText()
204
		// Text of title the action is aimed at.
205
		$target = $entry->getTarget()->getPrefixedText();
206
		$text = null;
207
		switch ( $entry->getType() ) {
208
			case 'move':
209
				switch ( $entry->getSubtype() ) {
210
					case 'move':
211
						$movesource = $parameters['4::target'];
212
						$text = wfMessage( '1movedto2' )
213
							->rawParams( $target, $movesource )->inContentLanguage()->escaped();
214
						break;
215
					case 'move_redir':
216
						$movesource = $parameters['4::target'];
217
						$text = wfMessage( '1movedto2_redir' )
218
							->rawParams( $target, $movesource )->inContentLanguage()->escaped();
219
						break;
220
					case 'move-noredirect':
221
						break;
222
					case 'move_redir-noredirect':
223
						break;
224
				}
225
				break;
226
227 View Code Duplication
			case 'delete':
228
				switch ( $entry->getSubtype() ) {
229
					case 'delete':
230
						$text = wfMessage( 'deletedarticle' )
231
							->rawParams( $target )->inContentLanguage()->escaped();
232
						break;
233
					case 'restore':
234
						$text = wfMessage( 'undeletedarticle' )
235
							->rawParams( $target )->inContentLanguage()->escaped();
236
						break;
237
					// @codingStandardsIgnoreStart Long line
238
					//case 'revision': // Revision deletion
239
					//case 'event': // Log deletion
240
					// see https://github.com/wikimedia/mediawiki/commit/a9c243b7b5289dad204278dbe7ed571fd914e395
241
					//default:
242
					// @codingStandardsIgnoreEnd
243
				}
244
				break;
245
246
			case 'patrol':
247
				// @codingStandardsIgnoreStart Long line
248
				// https://github.com/wikimedia/mediawiki/commit/1a05f8faf78675dc85984f27f355b8825b43efff
249
				// @codingStandardsIgnoreEnd
250
				// Create a diff link to the patrolled revision
251
				if ( $entry->getSubtype() === 'patrol' ) {
252
					$diffLink = htmlspecialchars(
253
						wfMessage( 'patrol-log-diff', $parameters['4::curid'] )
254
							->inContentLanguage()->text() );
255
					$text = wfMessage( 'patrol-log-line', $diffLink, "[[$target]]", "" )
256
						->inContentLanguage()->text();
257
				} else {
258
					// broken??
259
				}
260
				break;
261
262
			case 'protect':
263
				switch ( $entry->getSubtype() ) {
264
					case 'protect':
265
						$text = wfMessage( 'protectedarticle' )
266
							->rawParams( $target . ' ' . $parameters['4::description'] )->inContentLanguage()->escaped();
267
						break;
268
					case 'unprotect':
269
						$text = wfMessage( 'unprotectedarticle' )
270
							->rawParams( $target )->inContentLanguage()->escaped();
271
						break;
272
					case 'modify':
273
						$text = wfMessage( 'modifiedarticleprotection' )
274
							->rawParams( $target . ' ' . $parameters['4::description'] )->inContentLanguage()->escaped();
275
						break;
276
					case 'move_prot':
277
						$text = wfMessage( 'movedarticleprotection' )
278
							->rawParams( $target, $parameters['4::oldtitle'] )->inContentLanguage()->escaped();
279
						break;
280
				}
281
				break;
282
283
			case 'newusers':
284
				switch ( $entry->getSubtype() ) {
285
					case 'newusers':
286
					case 'create':
287
						$text = wfMessage( 'newuserlog-create-entry' )
288
							->inContentLanguage()->escaped();
289
						break;
290
					case 'create2':
291
					case 'byemail':
292
						$text = wfMessage( 'newuserlog-create2-entry' )
293
							->rawParams( $target )->inContentLanguage()->escaped();
294
						break;
295
					case 'autocreate':
296
						$text = wfMessage( 'newuserlog-autocreate-entry' )
297
							->inContentLanguage()->escaped();
298
						break;
299
				}
300
				break;
301
302 View Code Duplication
			case 'upload':
303
				switch ( $entry->getSubtype() ) {
304
					case 'upload':
305
						$text = wfMessage( 'uploadedimage' )
306
							->rawParams( $target )->inContentLanguage()->escaped();
307
						break;
308
					case 'overwrite':
309
						$text = wfMessage( 'overwroteimage' )
310
							->rawParams( $target )->inContentLanguage()->escaped();
311
						break;
312
				}
313
				break;
314
315
			case 'rights':
316 View Code Duplication
				if ( count( $parameters['4::oldgroups'] ) ) {
317
					$oldgroups = implode( ', ', $parameters['4::oldgroups'] );
318
				} else {
319
					$oldgroups = wfMessage( 'rightsnone' )->inContentLanguage()->escaped();
320
				}
321 View Code Duplication
				if ( count( $parameters['5::newgroups'] ) ) {
322
					$newgroups = implode( ', ', $parameters['5::newgroups'] );
323
				} else {
324
					$newgroups = wfMessage( 'rightsnone' )->inContentLanguage()->escaped();
325
				}
326
				switch ( $entry->getSubtype() ) {
327
					case 'rights':
328
						$text = wfMessage( 'rightslogentry' )
329
							->rawParams( $target, $oldgroups, $newgroups )->inContentLanguage()->escaped();
330
						break;
331
					case 'autopromote':
332
						$text = wfMessage( 'rightslogentry-autopromote' )
333
							->rawParams( $target, $oldgroups, $newgroups )->inContentLanguage()->escaped();
334
						break;
335
				}
336
				break;
337
338
			case 'merge':
339
				$text = wfMessage( 'pagemerge-logentry' )
340
					->rawParams( $target, $parameters['4::dest'], $parameters['5::mergepoint'] )
341
					->inContentLanguage()->escaped();
342
				break;
343
344
			case 'block':
345
				switch ( $entry->getSubtype() ) {
346
					case 'block':
347
						// Keep compatibility with extensions by checking for
348
						// new key (5::duration/6::flags) or old key (0/optional 1)
349
						if ( $entry->isLegacy() ) {
350
							$rawDuration = $parameters[0];
351
							$rawFlags = isset( $parameters[1] ) ? $parameters[1] : '';
352
						} else {
353
							$rawDuration = $parameters['5::duration'];
354
							$rawFlags = $parameters['6::flags'];
355
						}
356
						$duration = $wgContLang->translateBlockExpiry( $rawDuration );
357
						$flags = BlockLogFormatter::formatBlockFlags( $rawFlags, $wgContLang );
358
						$text = wfMessage( 'blocklogentry' )
359
							->rawParams( $target, $duration, $flags )->inContentLanguage()->escaped();
360
						break;
361
					case 'unblock':
362
						$text = wfMessage( 'unblocklogentry' )
363
							->rawParams( $target )->inContentLanguage()->escaped();
364
						break;
365
					case 'reblock':
366
						$duration = $wgContLang->translateBlockExpiry( $parameters['5::duration'] );
367
						$flags = BlockLogFormatter::formatBlockFlags( $parameters['6::flags'], $wgContLang );
368
						$text = wfMessage( 'reblock-logentry' )
369
							->rawParams( $target, $duration, $flags )->inContentLanguage()->escaped();
370
						break;
371
				}
372
				break;
373
374 View Code Duplication
			case 'import':
375
				switch ( $entry->getSubtype() ) {
376
					case 'upload':
377
						$text = wfMessage( 'import-logentry-upload' )
378
							->rawParams( $target )->inContentLanguage()->escaped();
379
						break;
380
					case 'interwiki':
381
						$text = wfMessage( 'import-logentry-interwiki' )
382
							->rawParams( $target )->inContentLanguage()->escaped();
383
						break;
384
				}
385
				break;
386
			// case 'suppress' --private log -- aaron  (so we know who to blame in a few years :-D)
387
			// default:
388
		}
389
		if ( is_null( $text ) ) {
390
			$text = $this->getPlainActionText();
391
		}
392
393
		$this->plaintext = false;
394
		$this->irctext = false;
395
396
		return $text;
397
	}
398
399
	/**
400
	 * Gets the log action, including username.
401
	 * @return string HTML
402
	 */
403
	public function getActionText() {
404
		if ( $this->canView( LogPage::DELETED_ACTION ) ) {
405
			$element = $this->getActionMessage();
406
			if ( $element instanceof Message ) {
407
				$element = $this->plaintext ? $element->text() : $element->escaped();
408
			}
409
			if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) {
410
				$element = $this->styleRestricedElement( $element );
411
			}
412
		} else {
413
			$sep = $this->msg( 'word-separator' );
414
			$sep = $this->plaintext ? $sep->text() : $sep->escaped();
415
			$performer = $this->getPerformerElement();
416
			$element = $performer . $sep . $this->getRestrictedElement( 'rev-deleted-event' );
417
		}
418
419
		return $element;
420
	}
421
422
	/**
423
	 * Returns a sentence describing the log action. Usually
424
	 * a Message object is returned, but old style log types
425
	 * and entries might return pre-escaped HTML string.
426
	 * @return Message|string Pre-escaped HTML
427
	 */
428
	protected function getActionMessage() {
429
		$message = $this->msg( $this->getMessageKey() );
430
		$message->params( $this->getMessageParameters() );
431
432
		return $message;
433
	}
434
435
	/**
436
	 * Returns a key to be used for formatting the action sentence.
437
	 * Default is logentry-TYPE-SUBTYPE for modern logs. Legacy log
438
	 * types will use custom keys, and subclasses can also alter the
439
	 * key depending on the entry itself.
440
	 * @return string Message key
441
	 */
442
	protected function getMessageKey() {
443
		$type = $this->entry->getType();
444
		$subtype = $this->entry->getSubtype();
445
446
		return "logentry-$type-$subtype";
447
	}
448
449
	/**
450
	 * Returns extra links that comes after the action text, like "revert", etc.
451
	 *
452
	 * @return string
453
	 */
454
	public function getActionLinks() {
455
		return '';
456
	}
457
458
	/**
459
	 * Extracts the optional extra parameters for use in action messages.
460
	 * The array indexes start from number 3.
461
	 * @return array
462
	 */
463
	protected function extractParameters() {
464
		$entry = $this->entry;
465
		$params = [];
466
467
		if ( $entry->isLegacy() ) {
468
			foreach ( $entry->getParameters() as $index => $value ) {
469
				$params[$index + 3] = $value;
470
			}
471
		}
472
473
		// Filter out parameters which are not in format #:foo
474
		foreach ( $entry->getParameters() as $key => $value ) {
475
			if ( strpos( $key, ':' ) === false ) {
476
				continue;
477
			}
478
			list( $index, $type, ) = explode( ':', $key, 3 );
479
			if ( ctype_digit( $index ) ) {
480
				$params[$index - 1] = $this->formatParameterValue( $type, $value );
481
			}
482
		}
483
484
		/* Message class doesn't like non consecutive numbering.
485
		 * Fill in missing indexes with empty strings to avoid
486
		 * incorrect renumbering.
487
		 */
488
		if ( count( $params ) ) {
489
			$max = max( array_keys( $params ) );
490
			// index 0 to 2 are added in getMessageParameters
491
			for ( $i = 3; $i < $max; $i++ ) {
492
				if ( !isset( $params[$i] ) ) {
493
					$params[$i] = '';
494
				}
495
			}
496
		}
497
498
		return $params;
499
	}
500
501
	/**
502
	 * Formats parameters intented for action message from
503
	 * array of all parameters. There are three hardcoded
504
	 * parameters (array is zero-indexed, this list not):
505
	 *  - 1: user name with premade link
506
	 *  - 2: usable for gender magic function
507
	 *  - 3: target page with premade link
508
	 * @return array
509
	 */
510
	protected function getMessageParameters() {
511
		if ( isset( $this->parsedParameters ) ) {
512
			return $this->parsedParameters;
513
		}
514
515
		$entry = $this->entry;
516
		$params = $this->extractParameters();
517
		$params[0] = Message::rawParam( $this->getPerformerElement() );
518
		$params[1] = $this->canView( LogPage::DELETED_USER ) ? $entry->getPerformer()->getName() : '';
519
		$params[2] = Message::rawParam( $this->makePageLink( $entry->getTarget() ) );
520
521
		// Bad things happens if the numbers are not in correct order
522
		ksort( $params );
523
524
		$this->parsedParameters = $params;
525
		return $this->parsedParameters;
526
	}
527
528
	/**
529
	 * Formats parameters values dependent to their type
530
	 * @param string $type The type of the value.
531
	 *   Valid are currently:
532
	 *     * - (empty) or plain: The value is returned as-is
533
	 *     * raw: The value will be added to the log message
534
	 *            as raw parameter (e.g. no escaping)
535
	 *            Use this only if there is no other working
536
	 *            type like user-link or title-link
537
	 *     * msg: The value is a message-key, the output is
538
	 *            the message in user language
539
	 *     * msg-content: The value is a message-key, the output
540
	 *                    is the message in content language
541
	 *     * user: The value is a user name, e.g. for GENDER
542
	 *     * user-link: The value is a user name, returns a
543
	 *                  link for the user
544
	 *     * title: The value is a page title,
545
	 *              returns name of page
546
	 *     * title-link: The value is a page title,
547
	 *                   returns link to this page
548
	 *     * number: Format value as number
549
	 *     * list: Format value as a comma-separated list
550
	 * @param mixed $value The parameter value that should be formatted
551
	 * @return string|array Formated value
552
	 * @since 1.21
553
	 */
554
	protected function formatParameterValue( $type, $value ) {
555
		$saveLinkFlood = $this->linkFlood;
556
557
		switch ( strtolower( trim( $type ) ) ) {
558
			case 'raw':
559
				$value = Message::rawParam( $value );
560
				break;
561
			case 'list':
562
				$value = $this->context->getLanguage()->commaList( $value );
563
				break;
564
			case 'msg':
565
				$value = $this->msg( $value )->text();
566
				break;
567
			case 'msg-content':
568
				$value = $this->msg( $value )->inContentLanguage()->text();
569
				break;
570
			case 'number':
571
				$value = Message::numParam( $value );
572
				break;
573
			case 'user':
574
				$user = User::newFromName( $value );
575
				$value = $user->getName();
576
				break;
577
			case 'user-link':
578
				$this->setShowUserToolLinks( false );
579
580
				$user = User::newFromName( $value );
581
				$value = Message::rawParam( $this->makeUserLink( $user ) );
0 ignored issues
show
It seems like $user defined by \User::newFromName($value) on line 580 can also be of type false; however, LogFormatter::makeUserLink() does only seem to accept object<User>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
582
583
				$this->setShowUserToolLinks( $saveLinkFlood );
584
				break;
585
			case 'title':
586
				$title = Title::newFromText( $value );
587
				$value = $title->getPrefixedText();
588
				break;
589
			case 'title-link':
590
				$title = Title::newFromText( $value );
591
				$value = Message::rawParam( $this->makePageLink( $title ) );
592
				break;
593
			case 'plain':
594
				// Plain text, nothing to do
595
			default:
596
				// Catch other types and use the old behavior (return as-is)
597
		}
598
599
		return $value;
600
	}
601
602
	/**
603
	 * Helper to make a link to the page, taking the plaintext
604
	 * value in consideration.
605
	 * @param Title $title The page
606
	 * @param array $parameters Query parameters
607
	 * @param string|null $html Linktext of the link as raw html
608
	 * @throws MWException
609
	 * @return string
610
	 */
611
	protected function makePageLink( Title $title = null, $parameters = [], $html = null ) {
612 View Code Duplication
		if ( !$this->plaintext ) {
613
			$link = Linker::link( $title, $html, [], $parameters );
614
		} else {
615
			if ( !$title instanceof Title ) {
616
				throw new MWException( "Expected title, got null" );
617
			}
618
			$link = '[[' . $title->getPrefixedText() . ']]';
619
		}
620
621
		return $link;
622
	}
623
624
	/**
625
	 * Provides the name of the user who performed the log action.
626
	 * Used as part of log action message or standalone, depending
627
	 * which parts of the log entry has been hidden.
628
	 * @return string
629
	 */
630 View Code Duplication
	public function getPerformerElement() {
631
		if ( $this->canView( LogPage::DELETED_USER ) ) {
632
			$performer = $this->entry->getPerformer();
633
			$element = $this->makeUserLink( $performer );
634
			if ( $this->entry->isDeleted( LogPage::DELETED_USER ) ) {
635
				$element = $this->styleRestricedElement( $element );
636
			}
637
		} else {
638
			$element = $this->getRestrictedElement( 'rev-deleted-user' );
639
		}
640
641
		return $element;
642
	}
643
644
	/**
645
	 * Gets the user provided comment
646
	 * @return string HTML
647
	 */
648 View Code Duplication
	public function getComment() {
649
		if ( $this->canView( LogPage::DELETED_COMMENT ) ) {
650
			$comment = Linker::commentBlock( $this->entry->getComment() );
651
			// No hard coded spaces thanx
652
			$element = ltrim( $comment );
653
			if ( $this->entry->isDeleted( LogPage::DELETED_COMMENT ) ) {
654
				$element = $this->styleRestricedElement( $element );
655
			}
656
		} else {
657
			$element = $this->getRestrictedElement( 'rev-deleted-comment' );
658
		}
659
660
		return $element;
661
	}
662
663
	/**
664
	 * Helper method for displaying restricted element.
665
	 * @param string $message
666
	 * @return string HTML or wiki text
667
	 */
668
	protected function getRestrictedElement( $message ) {
669
		if ( $this->plaintext ) {
670
			return $this->msg( $message )->text();
671
		}
672
673
		$content = $this->msg( $message )->escaped();
674
		$attribs = [ 'class' => 'history-deleted' ];
675
676
		return Html::rawElement( 'span', $attribs, $content );
677
	}
678
679
	/**
680
	 * Helper method for styling restricted element.
681
	 * @param string $content
682
	 * @return string HTML or wiki text
683
	 */
684
	protected function styleRestricedElement( $content ) {
685
		if ( $this->plaintext ) {
686
			return $content;
687
		}
688
		$attribs = [ 'class' => 'history-deleted' ];
689
690
		return Html::rawElement( 'span', $attribs, $content );
691
	}
692
693
	/**
694
	 * Shortcut for wfMessage which honors local context.
695
	 * @param string $key
696
	 * @return Message
697
	 */
698
	protected function msg( $key ) {
699
		return $this->context->msg( $key );
700
	}
701
702
	protected function makeUserLink( User $user, $toolFlags = 0 ) {
703
		if ( $this->plaintext ) {
704
			$element = $user->getName();
705
		} else {
706
			$element = Linker::userLink(
707
				$user->getId(),
708
				$user->getName()
709
			);
710
711
			if ( $this->linkFlood ) {
712
				$element .= Linker::userToolLinks(
713
					$user->getId(),
714
					$user->getName(),
715
					true, // redContribsWhenNoEdits
716
					$toolFlags,
717
					$user->getEditCount()
718
				);
719
			}
720
		}
721
722
		return $element;
723
	}
724
725
	/**
726
	 * @return array Array of titles that should be preloaded with LinkBatch
727
	 */
728
	public function getPreloadTitles() {
729
		return [];
730
	}
731
732
	/**
733
	 * @return array Output of getMessageParameters() for testing
734
	 */
735
	public function getMessageParametersForTesting() {
736
		// This function was added because getMessageParameters() is
737
		// protected and a change from protected to public caused
738
		// problems with extensions
739
		return $this->getMessageParameters();
740
	}
741
742
	/**
743
	 * Get the array of parameters, converted from legacy format if necessary.
744
	 * @since 1.25
745
	 * @return array
746
	 */
747
	protected function getParametersForApi() {
748
		return $this->entry->getParameters();
749
	}
750
751
	/**
752
	 * Format parameters for API output
753
	 *
754
	 * The result array should generally map named keys to values. Index and
755
	 * type should be omitted, e.g. "4::foo" should be returned as "foo" in the
756
	 * output. Values should generally be unformatted.
757
	 *
758
	 * Renames or removals of keys besides from the legacy numeric format to
759
	 * modern named style should be avoided. Any renames should be announced to
760
	 * the mediawiki-api-announce mailing list.
761
	 *
762
	 * @since 1.25
763
	 * @return array
764
	 */
765
	public function formatParametersForApi() {
766
		$logParams = [];
767
		foreach ( $this->getParametersForApi() as $key => $value ) {
768
			$vals = explode( ':', $key, 3 );
769
			if ( count( $vals ) !== 3 ) {
770
				$logParams[$key] = $value;
771
				continue;
772
			}
773
			$logParams += $this->formatParameterValueForApi( $vals[2], $vals[1], $value );
774
		}
775
		ApiResult::setIndexedTagName( $logParams, 'param' );
776
		ApiResult::setArrayType( $logParams, 'assoc' );
777
778
		return $logParams;
779
	}
780
781
	/**
782
	 * Format a single parameter value for API output
783
	 *
784
	 * @since 1.25
785
	 * @param string $name
786
	 * @param string $type
787
	 * @param string $value
788
	 * @return array
789
	 */
790
	protected function formatParameterValueForApi( $name, $type, $value ) {
791
		$type = strtolower( trim( $type ) );
792
		switch ( $type ) {
793
			case 'bool':
794
				$value = (bool)$value;
795
				break;
796
797
			case 'number':
798
				if ( ctype_digit( $value ) || is_int( $value ) ) {
799
					$value = (int)$value;
800
				} else {
801
					$value = (float)$value;
802
				}
803
				break;
804
805
			case 'array':
806
			case 'assoc':
807
			case 'kvp':
808
				if ( is_array( $value ) ) {
809
					ApiResult::setArrayType( $value, $type );
810
				}
811
				break;
812
813
			case 'timestamp':
814
				$value = wfTimestamp( TS_ISO_8601, $value );
815
				break;
816
817
			case 'msg':
818
			case 'msg-content':
819
				$msg = $this->msg( $value );
820
				if ( $type === 'msg-content' ) {
821
					$msg->inContentLanguage();
822
				}
823
				$value = [];
824
				$value["{$name}_key"] = $msg->getKey();
825
				if ( $msg->getParams() ) {
826
					$value["{$name}_params"] = $msg->getParams();
827
				}
828
				$value["{$name}_text"] = $msg->text();
829
				return $value;
830
831
			case 'title':
832
			case 'title-link':
833
				$title = Title::newFromText( $value );
834
				if ( $title ) {
835
					$value = [];
836
					ApiQueryBase::addTitleInfo( $value, $title, "{$name}_" );
837
				}
838
				return $value;
839
840
			case 'user':
841
			case 'user-link':
842
				$user = User::newFromName( $value );
843
				if ( $user ) {
844
					$value = $user->getName();
845
				}
846
				break;
847
848
			default:
849
				// do nothing
850
				break;
851
		}
852
853
		return [ $name => $value ];
854
	}
855
}
856
857
/**
858
 * This class formats all log entries for log types
859
 * which have not been converted to the new system.
860
 * This is not about old log entries which store
861
 * parameters in a different format - the new
862
 * LogFormatter classes have code to support formatting
863
 * those too.
864
 * @since 1.19
865
 */
866
class LegacyLogFormatter extends LogFormatter {
867
	/**
868
	 * Backward compatibility for extension changing the comment from
869
	 * the LogLine hook. This will be set by the first call on getComment(),
870
	 * then it might be modified by the hook when calling getActionLinks(),
871
	 * so that the modified value will be returned when calling getComment()
872
	 * a second time.
873
	 *
874
	 * @var string|null
875
	 */
876
	private $comment = null;
877
878
	/**
879
	 * Cache for the result of getActionLinks() so that it does not need to
880
	 * run multiple times depending on the order that getComment() and
881
	 * getActionLinks() are called.
882
	 *
883
	 * @var string|null
884
	 */
885
	private $revert = null;
886
887
	public function getComment() {
888
		if ( $this->comment === null ) {
889
			$this->comment = parent::getComment();
890
		}
891
892
		// Make sure we execute the LogLine hook so that we immediately return
893
		// the correct value.
894
		if ( $this->revert === null ) {
895
			$this->getActionLinks();
896
		}
897
898
		return $this->comment;
899
	}
900
901
	protected function getActionMessage() {
902
		$entry = $this->entry;
903
		$action = LogPage::actionText(
904
			$entry->getType(),
905
			$entry->getSubtype(),
906
			$entry->getTarget(),
907
			$this->plaintext ? null : $this->context->getSkin(),
908
			(array)$entry->getParameters(),
909
			!$this->plaintext // whether to filter [[]] links
910
		);
911
912
		$performer = $this->getPerformerElement();
913
		if ( !$this->irctext ) {
914
			$sep = $this->msg( 'word-separator' );
915
			$sep = $this->plaintext ? $sep->text() : $sep->escaped();
916
			$action = $performer . $sep . $action;
917
		}
918
919
		return $action;
920
	}
921
922
	public function getActionLinks() {
923
		if ( $this->revert !== null ) {
924
			return $this->revert;
925
		}
926
927
		if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) {
928
			$this->revert = '';
929
			return $this->revert;
930
		}
931
932
		$title = $this->entry->getTarget();
933
		$type = $this->entry->getType();
934
		$subtype = $this->entry->getSubtype();
935
936
		// Do nothing. The implementation is handled by the hook modifiying the
937
		// passed-by-ref parameters. This also changes the default value so that
938
		// getComment() and getActionLinks() do not call them indefinitely.
939
		$this->revert = '';
940
941
		// This is to populate the $comment member of this instance so that it
942
		// can be modified when calling the hook just below.
943
		if ( $this->comment === null ) {
944
			$this->getComment();
945
		}
946
947
		$params = $this->entry->getParameters();
948
949
		Hooks::run( 'LogLine', [ $type, $subtype, $title, $params,
950
			&$this->comment, &$this->revert, $this->entry->getTimestamp() ] );
951
952
		return $this->revert;
953
	}
954
}
955