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