1<?php 2 3use MediaWiki\MediaWikiServices; 4use MediaWiki\Revision\RevisionRecord; 5 6/** 7 * @group Database 8 */ 9class LinkerTest extends MediaWikiLangTestCase { 10 /** 11 * @dataProvider provideCasesForUserLink 12 * @covers Linker::userLink 13 */ 14 public function testUserLink( $expected, $userId, $userName, $altUserName = false, $msg = '' ) { 15 $this->setMwGlobals( [ 16 'wgArticlePath' => '/wiki/$1', 17 ] ); 18 19 // We'd also test the warning, but injecting a mock logger into a static method is tricky. 20 if ( !$userName ) { 21 Wikimedia\suppressWarnings(); 22 } 23 $actual = Linker::userLink( $userId, $userName, $altUserName ); 24 if ( !$userName ) { 25 Wikimedia\restoreWarnings(); 26 } 27 28 $this->assertEquals( $expected, $actual, $msg ); 29 } 30 31 public static function provideCasesForUserLink() { 32 # Format: 33 # - expected 34 # - userid 35 # - username 36 # - optional altUserName 37 # - optional message 38 return [ 39 # Empty name (T222529) 40 'Empty username, userid 0' => [ '(no username available)', 0, '' ], 41 'Empty username, userid > 0' => [ '(no username available)', 73, '' ], 42 43 'false instead of username' => [ '(no username available)', 73, false ], 44 'null instead of username' => [ '(no username available)', 0, null ], 45 46 # ## ANONYMOUS USER ######################################## 47 [ 48 '<a href="/wiki/Special:Contributions/JohnDoe" ' 49 . 'class="mw-userlink mw-anonuserlink" ' 50 . 'title="Special:Contributions/JohnDoe"><bdi>JohnDoe</bdi></a>', 51 0, 'JohnDoe', false, 52 ], 53 [ 54 '<a href="/wiki/Special:Contributions/::1" ' 55 . 'class="mw-userlink mw-anonuserlink" ' 56 . 'title="Special:Contributions/::1"><bdi>::1</bdi></a>', 57 0, '::1', false, 58 'Anonymous with pretty IPv6' 59 ], 60 [ 61 '<a href="/wiki/Special:Contributions/0:0:0:0:0:0:0:1" ' 62 . 'class="mw-userlink mw-anonuserlink" ' 63 . 'title="Special:Contributions/0:0:0:0:0:0:0:1"><bdi>::1</bdi></a>', 64 0, '0:0:0:0:0:0:0:1', false, 65 'Anonymous with almost pretty IPv6' 66 ], 67 [ 68 '<a href="/wiki/Special:Contributions/0000:0000:0000:0000:0000:0000:0000:0001" ' 69 . 'class="mw-userlink mw-anonuserlink" ' 70 . 'title="Special:Contributions/0000:0000:0000:0000:0000:0000:0000:0001"><bdi>::1</bdi></a>', 71 0, '0000:0000:0000:0000:0000:0000:0000:0001', false, 72 'Anonymous with full IPv6' 73 ], 74 [ 75 '<a href="/wiki/Special:Contributions/::1" ' 76 . 'class="mw-userlink mw-anonuserlink" ' 77 . 'title="Special:Contributions/::1"><bdi>AlternativeUsername</bdi></a>', 78 0, '::1', 'AlternativeUsername', 79 'Anonymous with pretty IPv6 and an alternative username' 80 ], 81 82 # IPV4 83 [ 84 '<a href="/wiki/Special:Contributions/127.0.0.1" ' 85 . 'class="mw-userlink mw-anonuserlink" ' 86 . 'title="Special:Contributions/127.0.0.1"><bdi>127.0.0.1</bdi></a>', 87 0, '127.0.0.1', false, 88 'Anonymous with IPv4' 89 ], 90 [ 91 '<a href="/wiki/Special:Contributions/127.0.0.1" ' 92 . 'class="mw-userlink mw-anonuserlink" ' 93 . 'title="Special:Contributions/127.0.0.1"><bdi>AlternativeUsername</bdi></a>', 94 0, '127.0.0.1', 'AlternativeUsername', 95 'Anonymous with IPv4 and an alternative username' 96 ], 97 98 # IP ranges 99 [ 100 '<a href="/wiki/Special:Contributions/1.2.3.4/31" ' 101 . 'class="mw-userlink mw-anonuserlink" ' 102 . 'title="Special:Contributions/1.2.3.4/31"><bdi>1.2.3.4/31</bdi></a>', 103 0, '1.2.3.4/31', false, 104 'Anonymous with IPv4 range' 105 ], 106 [ 107 '<a href="/wiki/Special:Contributions/2001:db8::1/43" ' 108 . 'class="mw-userlink mw-anonuserlink" ' 109 . 'title="Special:Contributions/2001:db8::1/43"><bdi>2001:db8::1/43</bdi></a>', 110 0, '2001:db8::1/43', false, 111 'Anonymous with IPv6 range' 112 ], 113 114 # External (imported) user, unknown prefix 115 [ 116 '<span class="mw-userlink mw-extuserlink mw-anonuserlink"><bdi>acme>Alice</bdi></span>', 117 0, "acme>Alice", false, 118 'User from acme wiki' 119 ], 120 121 # Corrupt user names 122 [ 123 "<span class=\"mw-userlink mw-anonuserlink\"><bdi>Foo\nBar</bdi></span>", 124 0, "Foo\nBar", false, 125 'User name with line break' 126 ], 127 [ 128 '<span class="mw-userlink mw-anonuserlink"><bdi>Barf_</bdi></span>', 129 0, "Barf_", false, 130 'User name with trailing underscore' 131 ], 132 [ 133 '<span class="mw-userlink mw-anonuserlink"><bdi>abcd</bdi></span>', 134 0, "abcd", false, 135 'Lower case user name' 136 ], 137 [ 138 '<span class="mw-userlink mw-anonuserlink"><bdi>For/Bar</bdi></span>', 139 0, "For/Bar", false, 140 'User name with slash' 141 ], 142 [ 143 '<span class="mw-userlink mw-anonuserlink"><bdi>For#Bar</bdi></span>', 144 0, "For#Bar", false, 145 'User name with hash' 146 ], 147 148 # ## Regular user ########################################## 149 # TODO! 150 ]; 151 } 152 153 /** 154 * @dataProvider provideUserToolLinks 155 * @covers Linker::userToolLinks 156 * @param string $expected 157 * @param int $userId 158 * @param string $userText 159 */ 160 public function testUserToolLinks( $expected, $userId, $userText ) { 161 // We'd also test the warning, but injecting a mock logger into a static method is tricky. 162 if ( $userText === '' ) { 163 Wikimedia\suppressWarnings(); 164 } 165 $actual = Linker::userToolLinks( $userId, $userText ); 166 if ( $userText === '' ) { 167 Wikimedia\restoreWarnings(); 168 } 169 170 $this->assertSame( $expected, $actual ); 171 } 172 173 public static function provideUserToolLinks() { 174 return [ 175 // Empty name (T222529) 176 'Empty username, userid 0' => [ ' (no username available)', 0, '' ], 177 'Empty username, userid > 0' => [ ' (no username available)', 73, '' ], 178 ]; 179 } 180 181 /** 182 * @dataProvider provideUserTalkLink 183 * @covers Linker::userTalkLink 184 * @param string $expected 185 * @param int $userId 186 * @param string $userText 187 */ 188 public function testUserTalkLink( $expected, $userId, $userText ) { 189 // We'd also test the warning, but injecting a mock logger into a static method is tricky. 190 if ( $userText === '' ) { 191 Wikimedia\suppressWarnings(); 192 } 193 $actual = Linker::userTalkLink( $userId, $userText ); 194 if ( $userText === '' ) { 195 Wikimedia\restoreWarnings(); 196 } 197 198 $this->assertSame( $expected, $actual ); 199 } 200 201 public static function provideUserTalkLink() { 202 return [ 203 // Empty name (T222529) 204 'Empty username, userid 0' => [ '(no username available)', 0, '' ], 205 'Empty username, userid > 0' => [ '(no username available)', 73, '' ], 206 ]; 207 } 208 209 /** 210 * @dataProvider provideBlockLink 211 * @covers Linker::blockLink 212 * @param string $expected 213 * @param int $userId 214 * @param string $userText 215 */ 216 public function testBlockLink( $expected, $userId, $userText ) { 217 // We'd also test the warning, but injecting a mock logger into a static method is tricky. 218 if ( $userText === '' ) { 219 Wikimedia\suppressWarnings(); 220 } 221 $actual = Linker::blockLink( $userId, $userText ); 222 if ( $userText === '' ) { 223 Wikimedia\restoreWarnings(); 224 } 225 226 $this->assertSame( $expected, $actual ); 227 } 228 229 public static function provideBlockLink() { 230 return [ 231 // Empty name (T222529) 232 'Empty username, userid 0' => [ '(no username available)', 0, '' ], 233 'Empty username, userid > 0' => [ '(no username available)', 73, '' ], 234 ]; 235 } 236 237 /** 238 * @dataProvider provideEmailLink 239 * @covers Linker::emailLink 240 * @param string $expected 241 * @param int $userId 242 * @param string $userText 243 */ 244 public function testEmailLink( $expected, $userId, $userText ) { 245 // We'd also test the warning, but injecting a mock logger into a static method is tricky. 246 if ( $userText === '' ) { 247 Wikimedia\suppressWarnings(); 248 } 249 $actual = Linker::emailLink( $userId, $userText ); 250 if ( $userText === '' ) { 251 Wikimedia\restoreWarnings(); 252 } 253 254 $this->assertSame( $expected, $actual ); 255 } 256 257 public static function provideEmailLink() { 258 return [ 259 // Empty name (T222529) 260 'Empty username, userid 0' => [ '(no username available)', 0, '' ], 261 'Empty username, userid > 0' => [ '(no username available)', 73, '' ], 262 ]; 263 } 264 265 /** 266 * @dataProvider provideCasesForFormatComment 267 * @covers Linker::formatComment 268 * @covers Linker::formatAutocomments 269 * @covers Linker::formatLinksInComment 270 */ 271 public function testFormatComment( 272 $expected, $comment, $title = false, $local = false, $wikiId = null 273 ) { 274 $conf = new SiteConfiguration(); 275 $conf->settings = [ 276 'wgServer' => [ 277 'enwiki' => '//en.example.org', 278 'dewiki' => '//de.example.org', 279 ], 280 'wgArticlePath' => [ 281 'enwiki' => '/w/$1', 282 'dewiki' => '/w/$1', 283 ], 284 ]; 285 $conf->suffixes = [ 'wiki' ]; 286 287 $this->setMwGlobals( [ 288 'wgScript' => '/wiki/index.php', 289 'wgArticlePath' => '/wiki/$1', 290 'wgCapitalLinks' => true, 291 'wgConf' => $conf, 292 // TODO: update tests when the default changes 293 'wgFragmentMode' => [ 'legacy' ], 294 ] ); 295 296 if ( $title === false ) { 297 // We need a page title that exists 298 $title = Title::newFromText( 'Special:BlankPage' ); 299 } 300 301 $this->assertEquals( 302 $expected, 303 Linker::formatComment( $comment, $title, $local, $wikiId ) 304 ); 305 } 306 307 public function provideCasesForFormatComment() { 308 $wikiId = 'enwiki'; // $wgConf has a fake entry for this 309 310 // phpcs:disable Generic.Files.LineLength 311 return [ 312 // Linker::formatComment 313 [ 314 'a<script>b', 315 'a<script>b', 316 ], 317 [ 318 'a—b', 319 'a—b', 320 ], 321 [ 322 "'''not bolded'''", 323 "'''not bolded'''", 324 ], 325 [ 326 "try <script>evil</scipt> things", 327 "try <script>evil</scipt> things", 328 ], 329 // Linker::formatAutocomments 330 [ 331 '<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→autocomment</a></span></span>', 332 "/* autocomment */", 333 ], 334 [ 335 '<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#linkie.3F" title="Special:BlankPage">→[[linkie?]]</a></span></span>', 336 "/* [[linkie?]] */", 337 ], 338 [ 339 '<span dir="auto"><span class="autocomment">: </span> // Edit via via</span>', 340 // Regression test for T222857 341 "/* */ // Edit via via", 342 ], 343 [ 344 '<span dir="auto"><span class="autocomment">: </span> foobar</span>', 345 // Regression test for T222857 346 "/**/ foobar", 347 ], 348 [ 349 '<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→autocomment</a>: </span> post</span>', 350 "/* autocomment */ post", 351 ], 352 [ 353 'pre <span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→autocomment</a></span></span>', 354 "pre /* autocomment */", 355 ], 356 [ 357 'pre <span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→autocomment</a>: </span> post</span>', 358 "pre /* autocomment */ post", 359 ], 360 [ 361 '<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→autocomment</a>: </span> multiple? <span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment2" title="Special:BlankPage">→autocomment2</a></span></span></span>', 362 "/* autocomment */ multiple? /* autocomment2 */", 363 ], 364 [ 365 '<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment_containing_.2F.2A" title="Special:BlankPage">→autocomment containing /*</a>: </span> T70361</span>', 366 "/* autocomment containing /* */ T70361" 367 ], 368 [ 369 '<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment_containing_.22quotes.22" title="Special:BlankPage">→autocomment containing "quotes"</a></span></span>', 370 "/* autocomment containing \"quotes\" */" 371 ], 372 [ 373 '<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment_containing_.3Cscript.3Etags.3C.2Fscript.3E" title="Special:BlankPage">→autocomment containing <script>tags</script></a></span></span>', 374 "/* autocomment containing <script>tags</script> */" 375 ], 376 [ 377 '<span dir="auto"><span class="autocomment"><a href="#autocomment">→autocomment</a></span></span>', 378 "/* autocomment */", 379 false, true 380 ], 381 [ 382 '<span dir="auto"><span class="autocomment">autocomment</span></span>', 383 "/* autocomment */", 384 null 385 ], 386 [ 387 '', 388 "/* */", 389 false, true 390 ], 391 [ 392 '', 393 "/* */", 394 null 395 ], 396 [ 397 '<span dir="auto"><span class="autocomment">[[</span></span>', 398 "/* [[ */", 399 false, true 400 ], 401 [ 402 '<span dir="auto"><span class="autocomment">[[</span></span>', 403 "/* [[ */", 404 null 405 ], 406 [ 407 "foo <span dir=\"auto\"><span class=\"autocomment\"><a href=\"#.23\">→[[#_\t_]]</a></span></span>", 408 "foo /* [[#_\t_]] */", 409 false, true 410 ], 411 [ 412 "foo <span dir=\"auto\"><span class=\"autocomment\"><a href=\"#_.09\">#_\t_</a></span></span>", 413 "foo /* [[#_\t_]] */", 414 null 415 ], 416 [ 417 '<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→autocomment</a></span></span>', 418 "/* autocomment */", 419 false, false 420 ], 421 [ 422 '<span dir="auto"><span class="autocomment"><a class="external" rel="nofollow" href="//en.example.org/w/Special:BlankPage#autocomment">→autocomment</a></span></span>', 423 "/* autocomment */", 424 false, false, $wikiId 425 ], 426 // Linker::formatLinksInComment 427 [ 428 'abc <a href="/wiki/index.php?title=Link&action=edit&redlink=1" class="new" title="Link (page does not exist)">link</a> def', 429 "abc [[link]] def", 430 ], 431 [ 432 'abc <a href="/wiki/index.php?title=Link&action=edit&redlink=1" class="new" title="Link (page does not exist)">text</a> def', 433 "abc [[link|text]] def", 434 ], 435 [ 436 'abc <a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a> def', 437 "abc [[Special:BlankPage|]] def", 438 ], 439 [ 440 'abc <a href="/wiki/index.php?title=%C4%84%C5%9B%C5%BC&action=edit&redlink=1" class="new" title="Ąśż (page does not exist)">ąśż</a> def', 441 "abc [[%C4%85%C5%9B%C5%BC]] def", 442 ], 443 [ 444 'abc <a href="/wiki/Special:BlankPage#section" title="Special:BlankPage">#section</a> def', 445 "abc [[#section]] def", 446 ], 447 [ 448 'abc <a href="/wiki/index.php?title=/subpage&action=edit&redlink=1" class="new" title="/subpage (page does not exist)">/subpage</a> def', 449 "abc [[/subpage]] def", 450 ], 451 [ 452 'abc <a href="/wiki/index.php?title=%22evil!%22&action=edit&redlink=1" class="new" title=""evil!" (page does not exist)">"evil!"</a> def', 453 "abc [[\"evil!\"]] def", 454 ], 455 [ 456 'abc [[<script>very evil</script>]] def', 457 "abc [[<script>very evil</script>]] def", 458 ], 459 [ 460 'abc [[|]] def', 461 "abc [[|]] def", 462 ], 463 [ 464 'abc <a href="/wiki/index.php?title=Link&action=edit&redlink=1" class="new" title="Link (page does not exist)">link</a> def', 465 "abc [[link]] def", 466 false, false 467 ], 468 [ 469 'abc <a class="external" rel="nofollow" href="//en.example.org/w/Link">link</a> def', 470 "abc [[link]] def", 471 false, false, $wikiId 472 ], 473 ]; 474 // phpcs:enable 475 } 476 477 /** 478 * @covers Linker::formatLinksInComment 479 * @dataProvider provideCasesForFormatLinksInComment 480 */ 481 public function testFormatLinksInComment( $expected, $input, $wiki ) { 482 $conf = new SiteConfiguration(); 483 $conf->settings = [ 484 'wgServer' => [ 485 'enwiki' => '//en.example.org' 486 ], 487 'wgArticlePath' => [ 488 'enwiki' => '/w/$1', 489 ], 490 ]; 491 $conf->suffixes = [ 'wiki' ]; 492 $this->setMwGlobals( [ 493 'wgScript' => '/wiki/index.php', 494 'wgArticlePath' => '/wiki/$1', 495 'wgCapitalLinks' => true, 496 'wgConf' => $conf, 497 ] ); 498 499 $this->assertEquals( 500 $expected, 501 Linker::formatLinksInComment( $input, Title::newFromText( 'Special:BlankPage' ), false, $wiki ) 502 ); 503 } 504 505 /** 506 * @covers Linker::generateRollback 507 * @dataProvider provideCasesForRollbackGeneration 508 */ 509 public function testGenerateRollback( $rollbackEnabled, $expectedModules, $title ) { 510 $this->markTestSkippedIfDbType( 'postgres' ); 511 512 $context = RequestContext::getMain(); 513 $user = $context->getUser(); 514 $user->setOption( 'showrollbackconfirmation', $rollbackEnabled ); 515 516 $this->assertSame( 0, Title::newFromText( $title )->getArticleID() ); 517 $pageData = $this->insertPage( $title ); 518 $page = WikiPage::factory( $pageData['title'] ); 519 520 $updater = $page->newPageUpdater( $user ); 521 $updater->setContent( \MediaWiki\Revision\SlotRecord::MAIN, 522 new TextContent( 'Technical Wishes 123!' ) 523 ); 524 $summary = CommentStoreComment::newUnsavedComment( 'Some comment!' ); 525 $updater->saveRevision( $summary ); 526 527 $rollbackOutput = Linker::generateRollback( $page->getRevisionRecord(), $context ); 528 $modules = $context->getOutput()->getModules(); 529 $currentRev = $page->getRevisionRecord(); 530 $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup(); 531 $oldestRev = $revisionLookup->getFirstRevision( $page->getTitle() ); 532 533 $this->assertEquals( $expectedModules, $modules ); 534 $this->assertInstanceOf( RevisionRecord::class, $currentRev ); 535 $this->assertInstanceOf( User::class, $currentRev->getUser() ); 536 $this->assertEquals( $user->getName(), $currentRev->getUser()->getName() ); 537 $this->assertEquals( 538 static::getTestSysop()->getUser(), 539 $oldestRev->getUser()->getName() 540 ); 541 542 $ids = []; 543 $r = $oldestRev; 544 while ( $r ) { 545 $ids[] = $r->getId(); 546 $r = $revisionLookup->getNextRevision( $r ); 547 } 548 $this->assertEquals( [ $oldestRev->getId(), $currentRev->getId() ], $ids ); 549 550 $this->assertStringContainsString( 'rollback 1 edit', $rollbackOutput ); 551 } 552 553 public static function provideCasesForRollbackGeneration() { 554 return [ 555 [ 556 true, 557 [ 'mediawiki.misc-authed-curate' ], 558 'Rollback_Test_Page' 559 ], 560 [ 561 false, 562 [], 563 'Rollback_Test_Page2' 564 ] 565 ]; 566 } 567 568 public static function provideCasesForFormatLinksInComment() { 569 // phpcs:disable Generic.Files.LineLength 570 return [ 571 [ 572 'foo bar <a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>', 573 'foo bar [[Special:BlankPage]]', 574 null, 575 ], 576 [ 577 '<a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>', 578 '[[ :Special:BlankPage]]', 579 null, 580 ], 581 [ 582 '[[Foo<a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>', 583 '[[Foo[[Special:BlankPage]]', 584 null, 585 ], 586 [ 587 '<a class="external" rel="nofollow" href="//en.example.org/w/Foo%27bar">Foo\'bar</a>', 588 "[[Foo'bar]]", 589 'enwiki', 590 ], 591 [ 592 'foo bar <a class="external" rel="nofollow" href="//en.example.org/w/Special:BlankPage">Special:BlankPage</a>', 593 'foo bar [[Special:BlankPage]]', 594 'enwiki', 595 ], 596 [ 597 'foo bar <a class="external" rel="nofollow" href="//en.example.org/w/File:Example">Image:Example</a>', 598 'foo bar [[Image:Example]]', 599 'enwiki', 600 ], 601 ]; 602 // phpcs:enable 603 } 604 605 public static function provideTooltipAndAccesskeyAttribs() { 606 return [ 607 'Watch no expiry' => [ 608 'ca-watch', [], null, [ 'title' => 'Add this page to your watchlist [w]', 'accesskey' => 'w' ] 609 ], 610 'Key does not exist' => [ 611 'key-does-not-exist', [], null, [] 612 ], 613 'Unwatch no expiry' => [ 614 'ca-unwatch', [], null, [ 'title' => 'Remove this page from your watchlist [w]', 615 'accesskey' => 'w' ] 616 ], 617 ]; 618 } 619 620 /** 621 * @covers Linker::tooltipAndAccesskeyAttribs 622 * @dataProvider provideTooltipAndAccesskeyAttribs 623 */ 624 public function testTooltipAndAccesskeyAttribs( $name, $msgParams, $options, $expected ) { 625 $this->setMwGlobals( [ 626 'wgWatchlistExpiry' => true, 627 ] ); 628 $user = $this->createMock( User::class ); 629 $user->method( 'isRegistered' )->willReturn( true ); 630 $user->method( 'isLoggedIn' )->willReturn( true ); 631 632 $title = SpecialPage::getTitleFor( 'Blankpage' ); 633 634 $context = RequestContext::getMain(); 635 $context->setTitle( $title ); 636 $context->setUser( $user ); 637 638 $watchedItemWithoutExpiry = new WatchedItem( $user, $title, null, null ); 639 640 $result = Linker::tooltipAndAccesskeyAttribs( $name, $msgParams, $options ); 641 642 $this->assertEquals( $expected, $result ); 643 } 644} 645