3 use MediaWiki\Api\ApiResult
;
4 use MediaWiki\Context\DerivativeContext
;
5 use MediaWiki\Context\RequestContext
;
6 use MediaWiki\Linker\Linker
;
7 use MediaWiki\MainConfigNames
;
8 use MediaWiki\Message\Message
;
9 use MediaWiki\Permissions\SimpleAuthority
;
10 use MediaWiki\RCFeed\IRCColourfulRCFeedFormatter
;
11 use MediaWiki\SpecialPage\SpecialPage
;
12 use MediaWiki\Title\Title
;
13 use MediaWiki\User\User
;
14 use MediaWiki\User\UserIdentityValue
;
19 class LogFormatterTest
extends MediaWikiLangTestCase
{
21 private static $oldExtMsgFiles;
46 protected $user_comment;
48 public static function setUpBeforeClass(): void
{
49 parent
::setUpBeforeClass();
51 global $wgExtensionMessagesFiles;
52 self
::$oldExtMsgFiles = $wgExtensionMessagesFiles;
53 $wgExtensionMessagesFiles['LogTests'] = __DIR__
. '/LogTests.i18n.php';
56 public static function tearDownAfterClass(): void
{
57 global $wgExtensionMessagesFiles;
58 $wgExtensionMessagesFiles = self
::$oldExtMsgFiles;
60 parent
::tearDownAfterClass();
63 protected function setUp(): void
{
66 $this->overrideConfigValues( [
67 MainConfigNames
::LogTypes
=> [ 'phpunit' ],
68 MainConfigNames
::LogActionsHandlers
=> [ 'phpunit/test' => LogFormatter
::class,
69 'phpunit/param' => LogFormatter
::class ],
72 $this->user
= User
::newFromName( 'Testuser' );
73 $this->title
= Title
::makeTitle( NS_MAIN
, 'SomeTitle' );
74 $this->target
= Title
::makeTitle( NS_MAIN
, 'TestTarget' );
76 $this->context
= new RequestContext();
77 $this->context
->setUser( $this->user
);
78 $this->context
->setTitle( $this->title
);
79 $this->context
->setLanguage( RequestContext
::getMain()->getLanguage() );
81 $this->user_comment
= '<User comment about action>';
84 public function newLogEntry( $action, $params ) {
85 $logEntry = new ManualLogEntry( 'phpunit', $action );
86 $logEntry->setPerformer( $this->user
);
87 $logEntry->setTarget( $this->title
);
88 $logEntry->setComment( 'A very good reason' );
90 $logEntry->setParameters( $params );
96 * @covers \LogFormatter::setShowUserToolLinks
98 public function testNormalLogParams() {
99 $entry = $this->newLogEntry( 'test', [] );
100 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
101 $formatter->setContext( $this->context
);
103 $formatter->setShowUserToolLinks( false );
104 $paramsWithoutTools = $formatter->getMessageParametersForTesting();
106 $formatter2 = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
107 $formatter2->setContext( $this->context
);
108 $formatter2->setShowUserToolLinks( true );
109 $paramsWithTools = $formatter2->getMessageParametersForTesting();
111 $userLink = Linker
::userLink(
112 $this->user
->getId(),
113 $this->user
->getName()
116 $userTools = Linker
::userToolLinksRedContribs(
117 $this->user
->getId(),
118 $this->user
->getName(),
119 $this->user
->getEditCount(),
123 $titleLink = Linker
::link( $this->title
, null, [], [] );
125 // $paramsWithoutTools and $paramsWithTools should be only different
127 $this->assertEquals( $paramsWithoutTools[1], $paramsWithTools[1] );
128 $this->assertEquals( $paramsWithoutTools[2], $paramsWithTools[2] );
130 $this->assertEquals( Message
::rawParam( $userLink ), $paramsWithoutTools[0] );
131 $this->assertEquals( Message
::rawParam( $userLink . $userTools ), $paramsWithTools[0] );
133 $this->assertEquals( $this->user
->getName(), $paramsWithoutTools[1] );
135 $this->assertEquals( Message
::rawParam( $titleLink ), $paramsWithoutTools[2] );
139 * @covers \LogFormatter::getActionText
141 public function testLogParamsTypeRaw() {
142 $params = [ '4:raw:raw' => Linker
::link( $this->title
, null, [], [] ) ];
143 $expected = Linker
::link( $this->title
, null, [], [] );
145 $entry = $this->newLogEntry( 'param', $params );
146 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
147 $formatter->setContext( $this->context
);
149 $logParam = $formatter->getActionText();
151 $this->assertEquals( $expected, $logParam );
155 * @covers \LogFormatter::getActionText
157 public function testLogParamsTypeMsg() {
158 $params = [ '4:msg:msg' => 'log-description-phpunit' ];
159 $expected = wfMessage( 'log-description-phpunit' )->text();
161 $entry = $this->newLogEntry( 'param', $params );
162 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
163 $formatter->setContext( $this->context
);
165 $logParam = $formatter->getActionText();
167 $this->assertEquals( $expected, $logParam );
171 * @covers \LogFormatter::getActionText
173 public function testLogParamsTypeMsgContent() {
174 $params = [ '4:msg-content:msgContent' => 'log-description-phpunit' ];
175 $expected = wfMessage( 'log-description-phpunit' )->inContentLanguage()->text();
177 $entry = $this->newLogEntry( 'param', $params );
178 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
179 $formatter->setContext( $this->context
);
181 $logParam = $formatter->getActionText();
183 $this->assertEquals( $expected, $logParam );
187 * @covers \LogFormatter::getActionText
189 public function testLogParamsTypeNumber() {
192 $params = [ '4:number:number' => 123456789 ];
193 $expected = $wgLang->formatNum( 123456789 );
195 $entry = $this->newLogEntry( 'param', $params );
196 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
197 $formatter->setContext( $this->context
);
199 $logParam = $formatter->getActionText();
201 $this->assertEquals( $expected, $logParam );
205 * @covers \LogFormatter::getActionText
207 public function testLogParamsTypeUserLink() {
208 $params = [ '4:user-link:userLink' => $this->user
->getName() ];
209 $expected = Linker
::userLink(
210 $this->user
->getId(),
211 $this->user
->getName()
214 $entry = $this->newLogEntry( 'param', $params );
215 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
216 $formatter->setContext( $this->context
);
218 $logParam = $formatter->getActionText();
220 $this->assertEquals( $expected, $logParam );
224 * @covers \LogFormatter::getActionText
226 public function testLogParamsTypeUserLink_empty() {
227 $params = [ '4:user-link:userLink' => ':' ];
229 $entry = $this->newLogEntry( 'param', $params );
230 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
232 $this->context
->setLanguage( 'qqx' );
233 $formatter->setContext( $this->context
);
235 $logParam = $formatter->getActionText();
236 $this->assertStringContainsString( '(empty-username)', $logParam );
240 * @covers \LogFormatter::getActionText
242 public function testLogParamsTypeTitleLink() {
243 $params = [ '4:title-link:titleLink' => $this->title
->getText() ];
244 $expected = Linker
::link( $this->title
, null, [], [] );
246 $entry = $this->newLogEntry( 'param', $params );
247 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
248 $formatter->setContext( $this->context
);
250 $logParam = $formatter->getActionText();
252 $this->assertEquals( $expected, $logParam );
256 * @covers \LogFormatter::getActionText
258 public function testLogParamsTypePlain() {
259 $params = [ '4:plain:plain' => 'Some plain text' ];
260 $expected = 'Some plain text';
262 $entry = $this->newLogEntry( 'param', $params );
263 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
264 $formatter->setContext( $this->context
);
266 $logParam = $formatter->getActionText();
268 $this->assertEquals( $expected, $logParam );
272 * @covers \LogFormatter::getPerformerElement
273 * @dataProvider provideLogElement
275 public function testGetPerformerElement( $deletedFlag, $allowedAction ) {
276 $entry = $this->newLogEntry( 'param', [] );
277 $entry->setPerformer( new UserIdentityValue( 1328435, 'Test' ) );
278 if ( $deletedFlag !== 'none' ) {
280 LogPage
::DELETED_USER |
281 ( $deletedFlag === 'suppressed' ? LogPage
::DELETED_RESTRICTED
: 0 )
285 $context = new DerivativeContext( $this->context
);
286 if ( $allowedAction !== 'none' ) {
287 $context->setAuthority( new SimpleAuthority(
288 $this->context
->getUser(),
289 [ $deletedFlag === 'suppressed' ?
'suppressrevision' : 'deletedhistory' ]
293 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
294 $formatter->setContext( $context );
295 if ( $allowedAction === 'view-for-user' ) {
296 $formatter->setAudience( LogFormatter
::FOR_THIS_USER
);
299 $element = $formatter->getPerformerElement();
300 if ( $allowedAction === 'none' ||
301 ( $deletedFlag !== 'none' && $allowedAction === 'view-public' )
303 $this->assertStringNotContainsString( 'User:Test', $element );
305 $this->assertStringContainsString( 'User:Test', $element );
308 if ( $deletedFlag === 'none' ) {
309 $this->assertStringNotContainsString( 'history-deleted', $element );
311 $this->assertStringContainsString( 'history-deleted', $element );
313 if ( $deletedFlag === 'suppressed' ) {
314 $this->assertStringContainsString( 'mw-history-suppressed', $element );
316 $this->assertStringNotContainsString( 'mw-history-suppressed', $element );
321 * @covers \LogFormatter::getComment
322 * @dataProvider provideLogElement
324 public function testLogComment( $deletedFlag, $allowedAction ) {
325 $entry = $this->newLogEntry( 'test', [] );
326 if ( $deletedFlag !== 'none' ) {
328 LogPage
::DELETED_COMMENT |
329 ( $deletedFlag === 'suppressed' ? LogPage
::DELETED_RESTRICTED
: 0 )
333 $context = new DerivativeContext( $this->context
);
334 if ( $allowedAction !== 'none' ) {
335 $context->setAuthority( new SimpleAuthority(
336 $this->context
->getUser(),
337 [ $deletedFlag === 'suppressed' ?
'suppressrevision' : 'deletedhistory' ]
341 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
342 $formatter->setContext( $context );
343 if ( $allowedAction === 'view-for-user' ) {
344 $formatter->setAudience( LogFormatter
::FOR_THIS_USER
);
347 $expectedComment = ltrim( $this->getServiceContainer()->getCommentFormatter()->formatBlock( $entry->getComment() ) );
348 $comment = $formatter->getComment();
350 if ( $allowedAction === 'none' ||
351 ( $deletedFlag !== 'none' && $allowedAction === 'view-public' )
353 $this->assertStringNotContainsString( $expectedComment, $comment );
355 $this->assertStringContainsString( $expectedComment, $comment );
357 if ( $deletedFlag === 'none' ) {
358 $this->assertStringNotContainsString( 'history-deleted', $comment );
360 $this->assertStringContainsString( 'history-deleted', $comment );
362 if ( $deletedFlag === 'suppressed' ) {
363 $this->assertStringContainsString( 'mw-history-suppressed', $comment );
365 $this->assertStringNotContainsString( 'mw-history-suppressed', $comment );
369 public static function provideLogElement() {
372 [ 'deleted', 'none' ],
373 [ 'deleted', 'view-for-user' ],
374 [ 'deleted', 'view-public' ],
375 [ 'suppressed', 'none' ],
376 [ 'suppressed', 'view-for-user' ],
377 [ 'suppressed', 'view-public' ],
382 * @dataProvider provideApiParamFormatting
383 * @covers \LogFormatter::formatParametersForApi
384 * @covers \LogFormatter::formatParameterValueForApi
386 public function testApiParamFormatting( $key, $value, $expected ) {
387 $entry = $this->newLogEntry( 'param', [ $key => $value ] );
388 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $entry );
389 $formatter->setContext( $this->context
);
391 ApiResult
::setIndexedTagName( $expected, 'param' );
392 ApiResult
::setArrayType( $expected, 'assoc' );
394 $this->assertEquals( $expected, $formatter->formatParametersForApi() );
397 public static function provideApiParamFormatting() {
399 [ 0, 'value', [ 'value' ] ],
400 [ 'named', 'value', [ 'named' => 'value' ] ],
401 [ '::key', 'value', [ 'key' => 'value' ] ],
402 [ '4::key', 'value', [ 'key' => 'value' ] ],
403 [ '4:raw:key', 'value', [ 'key' => 'value' ] ],
404 [ '4:plain:key', 'value', [ 'key' => 'value' ] ],
405 [ '4:bool:key', '1', [ 'key' => true ] ],
406 [ '4:bool:key', '0', [ 'key' => false ] ],
407 [ '4:number:key', '123', [ 'key' => 123 ] ],
408 [ '4:number:key', '123.5', [ 'key' => 123.5 ] ],
409 [ '4:array:key', [], [ 'key' => [ ApiResult
::META_TYPE
=> 'array' ] ] ],
410 [ '4:assoc:key', [], [ 'key' => [ ApiResult
::META_TYPE
=> 'assoc' ] ] ],
411 [ '4:kvp:key', [], [ 'key' => [ ApiResult
::META_TYPE
=> 'kvp' ] ] ],
412 [ '4:timestamp:key', '20150102030405', [ 'key' => '2015-01-02T03:04:05Z' ] ],
413 [ '4:msg:key', 'parentheses', [
414 'key_key' => 'parentheses',
415 'key_text' => wfMessage( 'parentheses' )->text(),
417 [ '4:msg-content:key', 'parentheses', [
418 'key_key' => 'parentheses',
419 'key_text' => wfMessage( 'parentheses' )->inContentLanguage()->text(),
421 [ '4:title:key', 'project:foo', [
422 'key_ns' => NS_PROJECT
,
423 'key_title' => Title
::makeTitle( NS_PROJECT
, 'Foo' )->getFullText(),
425 [ '4:title-link:key', 'project:foo', [
426 'key_ns' => NS_PROJECT
,
427 'key_title' => Title
::makeTitle( NS_PROJECT
, 'Foo' )->getFullText(),
429 [ '4:title-link:key', '<invalid>', [
430 'key_ns' => NS_SPECIAL
,
431 'key_title' => SpecialPage
::getTitleFor( 'Badtitle', '<invalid>' )->getFullText(),
433 [ '4:user:key', 'foo', [ 'key' => 'Foo' ] ],
434 [ '4:user-link:key', 'foo', [ 'key' => 'Foo' ] ],
439 * The testIrcMsgForAction* tests are supposed to cover the hacky
440 * LogFormatter::getIRCActionText / T36508
442 * Third parties bots listen to those messages. They are clever enough
443 * to fetch the i18n messages from the wiki and then analyze the IRC feed
444 * to reverse engineer the $1, $2 messages.
445 * One thing bots cannot detect is when MediaWiki change the meaning of
446 * a message like what happened when we deployed 1.19. $1 became the user
447 * performing the action which broke basically all bots around.
449 * Should cover the following log actions (which are most commonly used by bots):
457 * - newusers/autocreate
461 * - protect/modifyprotect
462 * - protect/unprotect
463 * - protect/move_prot
469 * As well as the following Auto Edit Summaries:
477 * @covers \LogFormatter::getIRCActionComment
478 * @covers \LogFormatter::getIRCActionText
480 public function testIrcMsgForLogTypeBlock() {
481 $sep = $this->context
->msg( 'colon-separator' )->text();
484 $this->assertIRCComment(
485 $this->context
->msg( 'blocklogentry', 'SomeTitle', 'duration', '(flags)' )->plain()
486 . $sep . $this->user_comment
,
489 '5::duration' => 'duration',
490 '6::flags' => 'flags',
494 # block/block - legacy
495 $this->assertIRCComment(
496 $this->context
->msg( 'blocklogentry', 'SomeTitle', 'duration', '(flags)' )->plain()
497 . $sep . $this->user_comment
,
508 $this->assertIRCComment(
509 $this->context
->msg( 'unblocklogentry', 'SomeTitle' )->plain() . $sep . $this->user_comment
,
515 $this->assertIRCComment(
516 $this->context
->msg( 'reblock-logentry', 'SomeTitle', 'duration', '(flags)' )->plain()
517 . $sep . $this->user_comment
,
520 '5::duration' => 'duration',
521 '6::flags' => 'flags',
528 * @covers \LogFormatter::getIRCActionComment
529 * @covers \LogFormatter::getIRCActionText
531 public function testIrcMsgForLogTypeDelete() {
532 $sep = $this->context
->msg( 'colon-separator' )->text();
535 $this->assertIRCComment(
536 $this->context
->msg( 'deletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment
,
543 $this->assertIRCComment(
544 $this->context
->msg( 'undeletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment
,
552 * @covers \LogFormatter::getIRCActionComment
553 * @covers \LogFormatter::getIRCActionText
555 public function testIrcMsgForLogTypeNewusers() {
556 $this->assertIRCComment(
558 'newusers', 'newusers',
561 $this->assertIRCComment(
563 'newusers', 'create',
566 $this->assertIRCComment(
567 'created new account SomeTitle',
568 'newusers', 'create2',
571 $this->assertIRCComment(
572 'Account created automatically',
573 'newusers', 'autocreate',
579 * @covers \LogFormatter::getIRCActionComment
580 * @covers \LogFormatter::getIRCActionText
582 public function testIrcMsgForLogTypeMove() {
584 '4::target' => $this->target
->getPrefixedText(),
587 $sep = $this->context
->msg( 'colon-separator' )->text();
590 $this->assertIRCComment(
591 $this->context
->msg( '1movedto2', 'SomeTitle', 'TestTarget' )
592 ->plain() . $sep . $this->user_comment
,
599 $this->assertIRCComment(
600 $this->context
->msg( '1movedto2_redir', 'SomeTitle', 'TestTarget' )
601 ->plain() . $sep . $this->user_comment
,
602 'move', 'move_redir',
609 * @covers \LogFormatter::getIRCActionComment
610 * @covers \LogFormatter::getIRCActionText
612 public function testIrcMsgForLogTypePatrol() {
614 $this->assertIRCComment(
615 $this->context
->msg( 'patrol-log-line', 'revision 777', '[[SomeTitle]]', '' )->plain(),
619 '5::previd' => '666',
626 * @covers \LogFormatter::getIRCActionComment
627 * @covers \LogFormatter::getIRCActionText
629 public function testIrcMsgForLogTypeProtect() {
631 '4::description' => '[edit=sysop] (indefinite) [move=sysop] (indefinite)'
633 $sep = $this->context
->msg( 'colon-separator' )->text();
636 $this->assertIRCComment(
637 $this->context
->msg( 'protectedarticle', 'SomeTitle ' . $protectParams['4::description'] )
638 ->plain() . $sep . $this->user_comment
,
639 'protect', 'protect',
645 $this->assertIRCComment(
646 $this->context
->msg( 'unprotectedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment
,
647 'protect', 'unprotect',
653 $this->assertIRCComment(
655 'modifiedarticleprotection',
656 'SomeTitle ' . $protectParams['4::description']
657 )->plain() . $sep . $this->user_comment
,
664 $this->assertIRCComment(
665 $this->context
->msg( 'movedarticleprotection', 'SomeTitle', 'OldTitle' )
666 ->plain() . $sep . $this->user_comment
,
667 'protect', 'move_prot',
669 '4::oldtitle' => 'OldTitle'
676 * @covers \LogFormatter::getIRCActionComment
677 * @covers \LogFormatter::getIRCActionText
679 public function testIrcMsgForLogTypeUpload() {
680 $sep = $this->context
->msg( 'colon-separator' )->text();
683 $this->assertIRCComment(
684 $this->context
->msg( 'uploadedimage', 'SomeTitle' )->plain() . $sep . $this->user_comment
,
691 $this->assertIRCComment(
692 $this->context
->msg( 'overwroteimage', 'SomeTitle' )->plain() . $sep . $this->user_comment
,
693 'upload', 'overwrite',
700 * @covers \LogFormatter::getIRCActionComment
701 * @covers \LogFormatter::getIRCActionText
703 public function testIrcMsgForLogTypeMerge() {
704 $sep = $this->context
->msg( 'colon-separator' )->text();
707 $this->assertIRCComment(
708 $this->context
->msg( 'pagemerge-logentry', 'SomeTitle', 'Dest', 'timestamp' )->plain()
709 . $sep . $this->user_comment
,
713 '5::mergepoint' => 'timestamp',
720 * @covers \LogFormatter::getIRCActionComment
721 * @covers \LogFormatter::getIRCActionText
723 public function testIrcMsgForLogTypeImport() {
724 $sep = $this->context
->msg( 'colon-separator' )->text();
727 $msg = $this->context
->msg( 'import-logentry-upload', 'SomeTitle' )->plain() .
730 $this->assertIRCComment(
738 $msg = $this->context
->msg( 'import-logentry-interwiki', 'SomeTitle' )->plain() .
741 $this->assertIRCComment(
743 'import', 'interwiki',
750 * @param string $expected Expected IRC text without colors codes
751 * @param string $type Log type (move, delete, suppress, patrol ...)
752 * @param string $action A log type action
753 * @param array $params
754 * @param string|null $comment A comment for the log action
756 * @param bool $legacy
758 protected function assertIRCComment( $expected, $type, $action, $params,
759 $comment = null, $msg = '', $legacy = false
761 $logEntry = new ManualLogEntry( $type, $action );
762 $logEntry->setPerformer( $this->user
);
763 $logEntry->setTarget( $this->title
);
764 if ( $comment !== null ) {
765 $logEntry->setComment( $comment );
767 $logEntry->setParameters( $params );
768 $logEntry->setLegacy( $legacy );
770 $formatter = $this->getServiceContainer()->getLogFormatterFactory()->newFromEntry( $logEntry );
771 $formatter->setContext( $this->context
);
773 // Apply the same transformation as done in IRCColourfulRCFeedFormatter::getLine for rc_comment
774 $ircRcComment = IRCColourfulRCFeedFormatter
::cleanupForIRC( $formatter->getIRCActionComment() );