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