Merge "editstash: segregate stats by content type for more useful graphing"
[lhc/web/wiklou.git] / tests / phpunit / includes / logging / LogFormatterTest.php
1 <?php
2
3 /**
4 * @group Database
5 */
6 class LogFormatterTest extends MediaWikiLangTestCase {
7 private static $oldExtMsgFiles;
8
9 /**
10 * @var User
11 */
12 protected $user;
13
14 /**
15 * @var Title
16 */
17 protected $title;
18
19 /**
20 * @var RequestContext
21 */
22 protected $context;
23
24 /**
25 * @var Title
26 */
27 protected $target;
28
29 /**
30 * @var string
31 */
32 protected $user_comment;
33
34 public static function setUpBeforeClass() {
35 parent::setUpBeforeClass();
36
37 global $wgExtensionMessagesFiles;
38 self::$oldExtMsgFiles = $wgExtensionMessagesFiles;
39 $wgExtensionMessagesFiles['LogTests'] = __DIR__ . '/LogTests.i18n.php';
40 Language::getLocalisationCache()->recache( 'en' );
41 }
42
43 public static function tearDownAfterClass() {
44 global $wgExtensionMessagesFiles;
45 $wgExtensionMessagesFiles = self::$oldExtMsgFiles;
46 Language::getLocalisationCache()->recache( 'en' );
47
48 parent::tearDownAfterClass();
49 }
50
51 protected function setUp() {
52 parent::setUp();
53
54 $this->setMwGlobals( [
55 'wgLogTypes' => [ 'phpunit' ],
56 'wgLogActionsHandlers' => [ 'phpunit/test' => LogFormatter::class,
57 'phpunit/param' => LogFormatter::class ],
58 'wgUser' => User::newFromName( 'Testuser' ),
59 ] );
60
61 $this->user = User::newFromName( 'Testuser' );
62 $this->title = Title::newFromText( 'SomeTitle' );
63 $this->target = Title::newFromText( 'TestTarget' );
64
65 $this->context = new RequestContext();
66 $this->context->setUser( $this->user );
67 $this->context->setTitle( $this->title );
68 $this->context->setLanguage( RequestContext::getMain()->getLanguage() );
69
70 $this->user_comment = '<User comment about action>';
71 }
72
73 public function newLogEntry( $action, $params ) {
74 $logEntry = new ManualLogEntry( 'phpunit', $action );
75 $logEntry->setPerformer( $this->user );
76 $logEntry->setTarget( $this->title );
77 $logEntry->setComment( 'A very good reason' );
78
79 $logEntry->setParameters( $params );
80
81 return $logEntry;
82 }
83
84 /**
85 * @covers LogFormatter::newFromEntry
86 */
87 public function testNormalLogParams() {
88 $entry = $this->newLogEntry( 'test', [] );
89 $formatter = LogFormatter::newFromEntry( $entry );
90 $formatter->setContext( $this->context );
91
92 $formatter->setShowUserToolLinks( false );
93 $paramsWithoutTools = $formatter->getMessageParametersForTesting();
94
95 $formatter2 = LogFormatter::newFromEntry( $entry );
96 $formatter2->setContext( $this->context );
97 $formatter2->setShowUserToolLinks( true );
98 $paramsWithTools = $formatter2->getMessageParametersForTesting();
99
100 $userLink = Linker::userLink(
101 $this->user->getId(),
102 $this->user->getName()
103 );
104
105 $userTools = Linker::userToolLinksRedContribs(
106 $this->user->getId(),
107 $this->user->getName(),
108 $this->user->getEditCount(),
109 false
110 );
111
112 $titleLink = Linker::link( $this->title, null, [], [] );
113
114 // $paramsWithoutTools and $paramsWithTools should be only different
115 // in index 0
116 $this->assertEquals( $paramsWithoutTools[1], $paramsWithTools[1] );
117 $this->assertEquals( $paramsWithoutTools[2], $paramsWithTools[2] );
118
119 $this->assertEquals( $userLink, $paramsWithoutTools[0]['raw'] );
120 $this->assertEquals( $userLink . $userTools, $paramsWithTools[0]['raw'] );
121
122 $this->assertEquals( $this->user->getName(), $paramsWithoutTools[1] );
123
124 $this->assertEquals( $titleLink, $paramsWithoutTools[2]['raw'] );
125 }
126
127 /**
128 * @covers LogFormatter::newFromEntry
129 * @covers LogFormatter::getActionText
130 */
131 public function testLogParamsTypeRaw() {
132 $params = [ '4:raw:raw' => Linker::link( $this->title, null, [], [] ) ];
133 $expected = Linker::link( $this->title, null, [], [] );
134
135 $entry = $this->newLogEntry( 'param', $params );
136 $formatter = LogFormatter::newFromEntry( $entry );
137 $formatter->setContext( $this->context );
138
139 $logParam = $formatter->getActionText();
140
141 $this->assertEquals( $expected, $logParam );
142 }
143
144 /**
145 * @covers LogFormatter::newFromEntry
146 * @covers LogFormatter::getActionText
147 */
148 public function testLogParamsTypeMsg() {
149 $params = [ '4:msg:msg' => 'log-description-phpunit' ];
150 $expected = wfMessage( 'log-description-phpunit' )->text();
151
152 $entry = $this->newLogEntry( 'param', $params );
153 $formatter = LogFormatter::newFromEntry( $entry );
154 $formatter->setContext( $this->context );
155
156 $logParam = $formatter->getActionText();
157
158 $this->assertEquals( $expected, $logParam );
159 }
160
161 /**
162 * @covers LogFormatter::newFromEntry
163 * @covers LogFormatter::getActionText
164 */
165 public function testLogParamsTypeMsgContent() {
166 $params = [ '4:msg-content:msgContent' => 'log-description-phpunit' ];
167 $expected = wfMessage( 'log-description-phpunit' )->inContentLanguage()->text();
168
169 $entry = $this->newLogEntry( 'param', $params );
170 $formatter = LogFormatter::newFromEntry( $entry );
171 $formatter->setContext( $this->context );
172
173 $logParam = $formatter->getActionText();
174
175 $this->assertEquals( $expected, $logParam );
176 }
177
178 /**
179 * @covers LogFormatter::newFromEntry
180 * @covers LogFormatter::getActionText
181 */
182 public function testLogParamsTypeNumber() {
183 global $wgLang;
184
185 $params = [ '4:number:number' => 123456789 ];
186 $expected = $wgLang->formatNum( 123456789 );
187
188 $entry = $this->newLogEntry( 'param', $params );
189 $formatter = LogFormatter::newFromEntry( $entry );
190 $formatter->setContext( $this->context );
191
192 $logParam = $formatter->getActionText();
193
194 $this->assertEquals( $expected, $logParam );
195 }
196
197 /**
198 * @covers LogFormatter::newFromEntry
199 * @covers LogFormatter::getActionText
200 */
201 public function testLogParamsTypeUserLink() {
202 $params = [ '4:user-link:userLink' => $this->user->getName() ];
203 $expected = Linker::userLink(
204 $this->user->getId(),
205 $this->user->getName()
206 );
207
208 $entry = $this->newLogEntry( 'param', $params );
209 $formatter = LogFormatter::newFromEntry( $entry );
210 $formatter->setContext( $this->context );
211
212 $logParam = $formatter->getActionText();
213
214 $this->assertEquals( $expected, $logParam );
215 }
216
217 /**
218 * @covers LogFormatter::newFromEntry
219 * @covers LogFormatter::getActionText
220 */
221 public function testLogParamsTypeTitleLink() {
222 $params = [ '4:title-link:titleLink' => $this->title->getText() ];
223 $expected = Linker::link( $this->title, null, [], [] );
224
225 $entry = $this->newLogEntry( 'param', $params );
226 $formatter = LogFormatter::newFromEntry( $entry );
227 $formatter->setContext( $this->context );
228
229 $logParam = $formatter->getActionText();
230
231 $this->assertEquals( $expected, $logParam );
232 }
233
234 /**
235 * @covers LogFormatter::newFromEntry
236 * @covers LogFormatter::getActionText
237 */
238 public function testLogParamsTypePlain() {
239 $params = [ '4:plain:plain' => 'Some plain text' ];
240 $expected = 'Some plain text';
241
242 $entry = $this->newLogEntry( 'param', $params );
243 $formatter = LogFormatter::newFromEntry( $entry );
244 $formatter->setContext( $this->context );
245
246 $logParam = $formatter->getActionText();
247
248 $this->assertEquals( $expected, $logParam );
249 }
250
251 /**
252 * @covers LogFormatter::newFromEntry
253 * @covers LogFormatter::getComment
254 */
255 public function testLogComment() {
256 $entry = $this->newLogEntry( 'test', [] );
257 $formatter = LogFormatter::newFromEntry( $entry );
258 $formatter->setContext( $this->context );
259
260 $comment = ltrim( Linker::commentBlock( $entry->getComment() ) );
261
262 $this->assertEquals( $comment, $formatter->getComment() );
263 }
264
265 /**
266 * @dataProvider provideApiParamFormatting
267 * @covers LogFormatter::formatParametersForApi
268 * @covers LogFormatter::formatParameterValueForApi
269 */
270 public function testApiParamFormatting( $key, $value, $expected ) {
271 $entry = $this->newLogEntry( 'param', [ $key => $value ] );
272 $formatter = LogFormatter::newFromEntry( $entry );
273 $formatter->setContext( $this->context );
274
275 ApiResult::setIndexedTagName( $expected, 'param' );
276 ApiResult::setArrayType( $expected, 'assoc' );
277
278 $this->assertEquals( $expected, $formatter->formatParametersForApi() );
279 }
280
281 public static function provideApiParamFormatting() {
282 return [
283 [ 0, 'value', [ 'value' ] ],
284 [ 'named', 'value', [ 'named' => 'value' ] ],
285 [ '::key', 'value', [ 'key' => 'value' ] ],
286 [ '4::key', 'value', [ 'key' => 'value' ] ],
287 [ '4:raw:key', 'value', [ 'key' => 'value' ] ],
288 [ '4:plain:key', 'value', [ 'key' => 'value' ] ],
289 [ '4:bool:key', '1', [ 'key' => true ] ],
290 [ '4:bool:key', '0', [ 'key' => false ] ],
291 [ '4:number:key', '123', [ 'key' => 123 ] ],
292 [ '4:number:key', '123.5', [ 'key' => 123.5 ] ],
293 [ '4:array:key', [], [ 'key' => [ ApiResult::META_TYPE => 'array' ] ] ],
294 [ '4:assoc:key', [], [ 'key' => [ ApiResult::META_TYPE => 'assoc' ] ] ],
295 [ '4:kvp:key', [], [ 'key' => [ ApiResult::META_TYPE => 'kvp' ] ] ],
296 [ '4:timestamp:key', '20150102030405', [ 'key' => '2015-01-02T03:04:05Z' ] ],
297 [ '4:msg:key', 'parentheses', [
298 'key_key' => 'parentheses',
299 'key_text' => wfMessage( 'parentheses' )->text(),
300 ] ],
301 [ '4:msg-content:key', 'parentheses', [
302 'key_key' => 'parentheses',
303 'key_text' => wfMessage( 'parentheses' )->inContentLanguage()->text(),
304 ] ],
305 [ '4:title:key', 'project:foo', [
306 'key_ns' => NS_PROJECT,
307 'key_title' => Title::newFromText( 'project:foo' )->getFullText(),
308 ] ],
309 [ '4:title-link:key', 'project:foo', [
310 'key_ns' => NS_PROJECT,
311 'key_title' => Title::newFromText( 'project:foo' )->getFullText(),
312 ] ],
313 [ '4:title-link:key', '<invalid>', [
314 'key_ns' => NS_SPECIAL,
315 'key_title' => SpecialPage::getTitleFor( 'Badtitle', '<invalid>' )->getFullText(),
316 ] ],
317 [ '4:user:key', 'foo', [ 'key' => 'Foo' ] ],
318 [ '4:user-link:key', 'foo', [ 'key' => 'Foo' ] ],
319 ];
320 }
321
322 /**
323 * The testIrcMsgForAction* tests are supposed to cover the hacky
324 * LogFormatter::getIRCActionText / T36508
325 *
326 * Third parties bots listen to those messages. They are clever enough
327 * to fetch the i18n messages from the wiki and then analyze the IRC feed
328 * to reverse engineer the $1, $2 messages.
329 * One thing bots can not detect is when MediaWiki change the meaning of
330 * a message like what happened when we deployed 1.19. $1 became the user
331 * performing the action which broke basically all bots around.
332 *
333 * Should cover the following log actions (which are most commonly used by bots):
334 * - block/block
335 * - block/unblock
336 * - block/reblock
337 * - delete/delete
338 * - delete/restore
339 * - newusers/create
340 * - newusers/create2
341 * - newusers/autocreate
342 * - move/move
343 * - move/move_redir
344 * - protect/protect
345 * - protect/modifyprotect
346 * - protect/unprotect
347 * - protect/move_prot
348 * - upload/upload
349 * - merge/merge
350 * - import/upload
351 * - import/interwiki
352 *
353 * As well as the following Auto Edit Summaries:
354 * - blank
355 * - replace
356 * - rollback
357 * - undo
358 */
359
360 /**
361 * @covers LogFormatter::getIRCActionComment
362 * @covers LogFormatter::getIRCActionText
363 */
364 public function testIrcMsgForLogTypeBlock() {
365 $sep = $this->context->msg( 'colon-separator' )->text();
366
367 # block/block
368 $this->assertIRCComment(
369 $this->context->msg( 'blocklogentry', 'SomeTitle', 'duration', '(flags)' )->plain()
370 . $sep . $this->user_comment,
371 'block', 'block',
372 [
373 '5::duration' => 'duration',
374 '6::flags' => 'flags',
375 ],
376 $this->user_comment
377 );
378 # block/block - legacy
379 $this->assertIRCComment(
380 $this->context->msg( 'blocklogentry', 'SomeTitle', 'duration', '(flags)' )->plain()
381 . $sep . $this->user_comment,
382 'block', 'block',
383 [
384 'duration',
385 'flags',
386 ],
387 $this->user_comment,
388 '',
389 true
390 );
391 # block/unblock
392 $this->assertIRCComment(
393 $this->context->msg( 'unblocklogentry', 'SomeTitle' )->plain() . $sep . $this->user_comment,
394 'block', 'unblock',
395 [],
396 $this->user_comment
397 );
398 # block/reblock
399 $this->assertIRCComment(
400 $this->context->msg( 'reblock-logentry', 'SomeTitle', 'duration', '(flags)' )->plain()
401 . $sep . $this->user_comment,
402 'block', 'reblock',
403 [
404 '5::duration' => 'duration',
405 '6::flags' => 'flags',
406 ],
407 $this->user_comment
408 );
409 }
410
411 /**
412 * @covers LogFormatter::getIRCActionComment
413 * @covers LogFormatter::getIRCActionText
414 */
415 public function testIrcMsgForLogTypeDelete() {
416 $sep = $this->context->msg( 'colon-separator' )->text();
417
418 # delete/delete
419 $this->assertIRCComment(
420 $this->context->msg( 'deletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment,
421 'delete', 'delete',
422 [],
423 $this->user_comment
424 );
425
426 # delete/restore
427 $this->assertIRCComment(
428 $this->context->msg( 'undeletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment,
429 'delete', 'restore',
430 [],
431 $this->user_comment
432 );
433 }
434
435 /**
436 * @covers LogFormatter::getIRCActionComment
437 * @covers LogFormatter::getIRCActionText
438 */
439 public function testIrcMsgForLogTypeNewusers() {
440 $this->assertIRCComment(
441 'New user account',
442 'newusers', 'newusers',
443 []
444 );
445 $this->assertIRCComment(
446 'New user account',
447 'newusers', 'create',
448 []
449 );
450 $this->assertIRCComment(
451 'created new account SomeTitle',
452 'newusers', 'create2',
453 []
454 );
455 $this->assertIRCComment(
456 'Account created automatically',
457 'newusers', 'autocreate',
458 []
459 );
460 }
461
462 /**
463 * @covers LogFormatter::getIRCActionComment
464 * @covers LogFormatter::getIRCActionText
465 */
466 public function testIrcMsgForLogTypeMove() {
467 $move_params = [
468 '4::target' => $this->target->getPrefixedText(),
469 '5::noredir' => 0,
470 ];
471 $sep = $this->context->msg( 'colon-separator' )->text();
472
473 # move/move
474 $this->assertIRCComment(
475 $this->context->msg( '1movedto2', 'SomeTitle', 'TestTarget' )
476 ->plain() . $sep . $this->user_comment,
477 'move', 'move',
478 $move_params,
479 $this->user_comment
480 );
481
482 # move/move_redir
483 $this->assertIRCComment(
484 $this->context->msg( '1movedto2_redir', 'SomeTitle', 'TestTarget' )
485 ->plain() . $sep . $this->user_comment,
486 'move', 'move_redir',
487 $move_params,
488 $this->user_comment
489 );
490 }
491
492 /**
493 * @covers LogFormatter::getIRCActionComment
494 * @covers LogFormatter::getIRCActionText
495 */
496 public function testIrcMsgForLogTypePatrol() {
497 # patrol/patrol
498 $this->assertIRCComment(
499 $this->context->msg( 'patrol-log-line', 'revision 777', '[[SomeTitle]]', '' )->plain(),
500 'patrol', 'patrol',
501 [
502 '4::curid' => '777',
503 '5::previd' => '666',
504 '6::auto' => 0,
505 ]
506 );
507 }
508
509 /**
510 * @covers LogFormatter::getIRCActionComment
511 * @covers LogFormatter::getIRCActionText
512 */
513 public function testIrcMsgForLogTypeProtect() {
514 $protectParams = [
515 '4::description' => '[edit=sysop] (indefinite) ‎[move=sysop] (indefinite)'
516 ];
517 $sep = $this->context->msg( 'colon-separator' )->text();
518
519 # protect/protect
520 $this->assertIRCComment(
521 $this->context->msg( 'protectedarticle', 'SomeTitle ' . $protectParams['4::description'] )
522 ->plain() . $sep . $this->user_comment,
523 'protect', 'protect',
524 $protectParams,
525 $this->user_comment
526 );
527
528 # protect/unprotect
529 $this->assertIRCComment(
530 $this->context->msg( 'unprotectedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment,
531 'protect', 'unprotect',
532 [],
533 $this->user_comment
534 );
535
536 # protect/modify
537 $this->assertIRCComment(
538 $this->context->msg(
539 'modifiedarticleprotection',
540 'SomeTitle ' . $protectParams['4::description']
541 )->plain() . $sep . $this->user_comment,
542 'protect', 'modify',
543 $protectParams,
544 $this->user_comment
545 );
546
547 # protect/move_prot
548 $this->assertIRCComment(
549 $this->context->msg( 'movedarticleprotection', 'SomeTitle', 'OldTitle' )
550 ->plain() . $sep . $this->user_comment,
551 'protect', 'move_prot',
552 [
553 '4::oldtitle' => 'OldTitle'
554 ],
555 $this->user_comment
556 );
557 }
558
559 /**
560 * @covers LogFormatter::getIRCActionComment
561 * @covers LogFormatter::getIRCActionText
562 */
563 public function testIrcMsgForLogTypeUpload() {
564 $sep = $this->context->msg( 'colon-separator' )->text();
565
566 # upload/upload
567 $this->assertIRCComment(
568 $this->context->msg( 'uploadedimage', 'SomeTitle' )->plain() . $sep . $this->user_comment,
569 'upload', 'upload',
570 [],
571 $this->user_comment
572 );
573
574 # upload/overwrite
575 $this->assertIRCComment(
576 $this->context->msg( 'overwroteimage', 'SomeTitle' )->plain() . $sep . $this->user_comment,
577 'upload', 'overwrite',
578 [],
579 $this->user_comment
580 );
581 }
582
583 /**
584 * @covers LogFormatter::getIRCActionComment
585 * @covers LogFormatter::getIRCActionText
586 */
587 public function testIrcMsgForLogTypeMerge() {
588 $sep = $this->context->msg( 'colon-separator' )->text();
589
590 # merge/merge
591 $this->assertIRCComment(
592 $this->context->msg( 'pagemerge-logentry', 'SomeTitle', 'Dest', 'timestamp' )->plain()
593 . $sep . $this->user_comment,
594 'merge', 'merge',
595 [
596 '4::dest' => 'Dest',
597 '5::mergepoint' => 'timestamp',
598 ],
599 $this->user_comment
600 );
601 }
602
603 /**
604 * @covers LogFormatter::getIRCActionComment
605 * @covers LogFormatter::getIRCActionText
606 */
607 public function testIrcMsgForLogTypeImport() {
608 $sep = $this->context->msg( 'colon-separator' )->text();
609
610 # import/upload
611 $msg = $this->context->msg( 'import-logentry-upload', 'SomeTitle' )->plain() .
612 $sep .
613 $this->user_comment;
614 $this->assertIRCComment(
615 $msg,
616 'import', 'upload',
617 [],
618 $this->user_comment
619 );
620
621 # import/interwiki
622 $msg = $this->context->msg( 'import-logentry-interwiki', 'SomeTitle' )->plain() .
623 $sep .
624 $this->user_comment;
625 $this->assertIRCComment(
626 $msg,
627 'import', 'interwiki',
628 [],
629 $this->user_comment
630 );
631 }
632
633 /**
634 * @param string $expected Expected IRC text without colors codes
635 * @param string $type Log type (move, delete, suppress, patrol ...)
636 * @param string $action A log type action
637 * @param array $params
638 * @param string $comment (optional) A comment for the log action
639 * @param string $msg (optional) A message for PHPUnit :-)
640 */
641 protected function assertIRCComment( $expected, $type, $action, $params,
642 $comment = null, $msg = '', $legacy = false
643 ) {
644 $logEntry = new ManualLogEntry( $type, $action );
645 $logEntry->setPerformer( $this->user );
646 $logEntry->setTarget( $this->title );
647 if ( $comment !== null ) {
648 $logEntry->setComment( $comment );
649 }
650 $logEntry->setParameters( $params );
651 $logEntry->setLegacy( $legacy );
652
653 $formatter = LogFormatter::newFromEntry( $logEntry );
654 $formatter->setContext( $this->context );
655
656 // Apply the same transformation as done in IRCColourfulRCFeedFormatter::getLine for rc_comment
657 $ircRcComment = IRCColourfulRCFeedFormatter::cleanupForIRC( $formatter->getIRCActionComment() );
658
659 $this->assertEquals(
660 $expected,
661 $ircRcComment,
662 $msg
663 );
664 }
665
666 }