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