1<?php 2 3use MediaWiki\Interwiki\ClassicInterwikiLookup; 4use MediaWiki\Interwiki\InterwikiLookup; 5use MediaWiki\Linker\LinkTarget; 6use MediaWiki\MediaWikiServices; 7use MediaWiki\Page\PageIdentity; 8use MediaWiki\Page\PageIdentityValue; 9use MediaWiki\User\UserIdentityValue; 10 11/** 12 * @group Database 13 * @group Title 14 */ 15class TitleTest extends MediaWikiIntegrationTestCase { 16 protected function setUp() : void { 17 parent::setUp(); 18 19 $this->setMwGlobals( [ 20 'wgAllowUserJs' => false, 21 'wgDefaultLanguageVariant' => false, 22 'wgMetaNamespace' => 'Project', 23 'wgServer' => 'https://example.org', 24 'wgCanonicalServer' => 'https://example.org', 25 'wgScriptPath' => '/w', 26 'wgScript' => '/w/index.php', 27 'wgArticlePath' => '/wiki/$1', 28 ] ); 29 $this->setUserLang( 'en' ); 30 $this->setContentLang( 'en' ); 31 } 32 33 protected function tearDown() : void { 34 parent::tearDown(); 35 // delete dummy pages 36 $this->getNonexistingTestPage( 'UTest1' ); 37 $this->getNonexistingTestPage( 'UTest2' ); 38 } 39 40 /** 41 * @covers Title::newFromID 42 * @covers Title::newFromIDs 43 * @covers Title::newFromRow 44 */ 45 public function testNewFromIds() { 46 // First id 47 $existingPage1 = $this->getExistingTestPage( 'UTest1' ); 48 $existingTitle1 = $existingPage1->getTitle(); 49 $existingId1 = $existingTitle1->getId(); 50 51 $this->assertGreaterThan( 0, $existingId1, 'Sanity: Existing test page should have a positive id' ); 52 53 $newFromId1 = Title::newFromID( $existingId1 ); 54 $this->assertInstanceOf( Title::class, $newFromId1, 'newFromID returns a title for an existing id' ); 55 $this->assertTrue( 56 $newFromId1->equals( $existingTitle1 ), 57 'newFromID returns the correct title' 58 ); 59 60 // Second id 61 $existingPage2 = $this->getExistingTestPage( 'UTest2' ); 62 $existingTitle2 = $existingPage2->getTitle(); 63 $existingId2 = $existingTitle2->getId(); 64 65 $this->assertGreaterThan( 0, $existingId2, 'Sanity: Existing test page should have a positive id' ); 66 67 $newFromId2 = Title::newFromID( $existingId2 ); 68 $this->assertInstanceOf( Title::class, $newFromId2, 'newFromID returns a title for an existing id' ); 69 $this->assertTrue( 70 $newFromId2->equals( $existingTitle2 ), 71 'newFromID returns the correct title' 72 ); 73 74 // newFromIDs using both 75 $titles = Title::newFromIDs( [ $existingId1, $existingId2 ] ); 76 $this->assertCount( 2, $titles ); 77 $this->assertTrue( 78 $titles[0]->equals( $existingTitle1 ) && 79 $titles[1]->equals( $existingTitle2 ), 80 'newFromIDs returns an array that matches the correct titles' 81 ); 82 83 // newFromIds early return for an empty array of ids 84 $this->assertSame( [], Title::newFromIDs( [] ) ); 85 } 86 87 /** 88 * @covers Title::newFromID 89 */ 90 public function testNewFromMissingId() { 91 // Testing return of null for an id that does not exist 92 $maxPageId = (int)$this->db->selectField( 93 'page', 94 'max(page_id)', 95 '', 96 __METHOD__ 97 ); 98 $res = Title::newFromId( $maxPageId + 1 ); 99 $this->assertNull( $res, 'newFromID returns null for missing ids' ); 100 } 101 102 /** 103 * @covers Title::legalChars 104 */ 105 public function testLegalChars() { 106 $titlechars = Title::legalChars(); 107 108 foreach ( range( 1, 255 ) as $num ) { 109 $chr = chr( $num ); 110 if ( strpos( "#[]{}<>|", $chr ) !== false || preg_match( "/[\\x00-\\x1f\\x7f]/", $chr ) ) { 111 $this->assertFalse( 112 (bool)preg_match( "/[$titlechars]/", $chr ), 113 "chr($num) = $chr is not a valid titlechar" 114 ); 115 } else { 116 $this->assertTrue( 117 (bool)preg_match( "/[$titlechars]/", $chr ), 118 "chr($num) = $chr is a valid titlechar" 119 ); 120 } 121 } 122 } 123 124 public static function provideValidSecureAndSplit() { 125 return [ 126 [ 'Sandbox' ], 127 [ 'A "B"' ], 128 [ 'A \'B\'' ], 129 [ '.com' ], 130 [ '~' ], 131 [ '#' ], 132 [ '"' ], 133 [ '\'' ], 134 [ 'Talk:Sandbox' ], 135 [ 'Talk:Foo:Sandbox' ], 136 [ 'File:Example.svg' ], 137 [ 'File_talk:Example.svg' ], 138 [ 'Foo/.../Sandbox' ], 139 [ 'Sandbox/...' ], 140 [ 'A~~' ], 141 [ ':A' ], 142 // Length is 256 total, but only title part matters 143 [ 'Category:' . str_repeat( 'x', 248 ) ], 144 [ str_repeat( 'x', 252 ) ], 145 // interwiki prefix 146 [ 'localtestiw: #anchor' ], 147 [ 'localtestiw:' ], 148 [ 'localtestiw:foo' ], 149 [ 'localtestiw: foo # anchor' ], 150 [ 'localtestiw: Talk: Sandbox # anchor' ], 151 [ 'remotetestiw:' ], 152 [ 'remotetestiw: Talk: # anchor' ], 153 [ 'remotetestiw: #bar' ], 154 [ 'remotetestiw: Talk:' ], 155 [ 'remotetestiw: Talk: Foo' ], 156 [ 'localtestiw:remotetestiw:' ], 157 [ 'localtestiw:remotetestiw:foo' ] 158 ]; 159 } 160 161 public static function provideInvalidSecureAndSplit() { 162 return [ 163 [ '', 'title-invalid-empty' ], 164 [ ':', 'title-invalid-empty' ], 165 [ '__ __', 'title-invalid-empty' ], 166 [ ' __ ', 'title-invalid-empty' ], 167 // Bad characters forbidden regardless of wgLegalTitleChars 168 [ 'A [ B', 'title-invalid-characters' ], 169 [ 'A ] B', 'title-invalid-characters' ], 170 [ 'A { B', 'title-invalid-characters' ], 171 [ 'A } B', 'title-invalid-characters' ], 172 [ 'A < B', 'title-invalid-characters' ], 173 [ 'A > B', 'title-invalid-characters' ], 174 [ 'A | B', 'title-invalid-characters' ], 175 [ "A \t B", 'title-invalid-characters' ], 176 [ "A \n B", 'title-invalid-characters' ], 177 // URL encoding 178 [ 'A%20B', 'title-invalid-characters' ], 179 [ 'A%23B', 'title-invalid-characters' ], 180 [ 'A%2523B', 'title-invalid-characters' ], 181 // XML/HTML character entity references 182 // Note: Commented out because they are not marked invalid by the PHP test as 183 // Title::newFromText runs Sanitizer::decodeCharReferencesAndNormalize first. 184 // 'A é B', 185 // 'A é B', 186 // 'A é B', 187 // Subject of NS_TALK does not roundtrip to NS_MAIN 188 [ 'Talk:File:Example.svg', 'title-invalid-talk-namespace' ], 189 // Directory navigation 190 [ '.', 'title-invalid-relative' ], 191 [ '..', 'title-invalid-relative' ], 192 [ './Sandbox', 'title-invalid-relative' ], 193 [ '../Sandbox', 'title-invalid-relative' ], 194 [ 'Foo/./Sandbox', 'title-invalid-relative' ], 195 [ 'Foo/../Sandbox', 'title-invalid-relative' ], 196 [ 'Sandbox/.', 'title-invalid-relative' ], 197 [ 'Sandbox/..', 'title-invalid-relative' ], 198 // Tilde 199 [ 'A ~~~ Name', 'title-invalid-magic-tilde' ], 200 [ 'A ~~~~ Signature', 'title-invalid-magic-tilde' ], 201 [ 'A ~~~~~ Timestamp', 'title-invalid-magic-tilde' ], 202 // Length 203 [ str_repeat( 'x', 256 ), 'title-invalid-too-long' ], 204 // Namespace prefix without actual title 205 [ 'Talk:', 'title-invalid-empty' ], 206 [ 'Talk:#', 'title-invalid-empty' ], 207 [ 'Category: ', 'title-invalid-empty' ], 208 [ 'Category: #bar', 'title-invalid-empty' ], 209 // interwiki prefix 210 [ 'localtestiw: Talk: # anchor', 'title-invalid-empty' ], 211 [ 'localtestiw: Talk:', 'title-invalid-empty' ] 212 ]; 213 } 214 215 private function secureAndSplitGlobals() { 216 $this->setMwGlobals( [ 217 'wgLocalInterwikis' => [ 'localtestiw' ], 218 'wgInterwikiCache' => ClassicInterwikiLookup::buildCdbHash( [ 219 [ 220 'iw_prefix' => 'localtestiw', 221 'iw_url' => 'localtestiw', 222 ], 223 [ 224 'iw_prefix' => 'remotetestiw', 225 'iw_url' => 'remotetestiw', 226 ], 227 ] ), 228 ] ); 229 } 230 231 /** 232 * See also mediawiki.Title.test.js 233 * @covers Title::secureAndSplit 234 * @dataProvider provideValidSecureAndSplit 235 * @note This mainly tests MediaWikiTitleCodec::parseTitle(). 236 */ 237 public function testSecureAndSplitValid( $text ) { 238 $this->secureAndSplitGlobals(); 239 $this->assertInstanceOf( Title::class, Title::newFromText( $text ), "Valid: $text" ); 240 } 241 242 /** 243 * See also mediawiki.Title.test.js 244 * @covers Title::secureAndSplit 245 * @dataProvider provideInvalidSecureAndSplit 246 * @note This mainly tests MediaWikiTitleCodec::parseTitle(). 247 */ 248 public function testSecureAndSplitInvalid( $text, $expectedErrorMessage ) { 249 $this->secureAndSplitGlobals(); 250 try { 251 Title::newFromTextThrow( $text ); // should throw 252 $this->fail( "Title::newFromTextThrow should have thrown with $text" ); 253 } catch ( MalformedTitleException $ex ) { 254 $this->assertEquals( $expectedErrorMessage, $ex->getErrorMessage(), "Invalid: $text" ); 255 } 256 } 257 258 public static function provideConvertByteClassToUnicodeClass() { 259 return [ 260 [ 261 ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+', 262 ' %!"$&\'()*,\\-./0-9:;=?@A-Z\\\\\\^_`a-z~+\\u0080-\\uFFFF', 263 ], 264 [ 265 'QWERTYf-\\xFF+', 266 'QWERTYf-\\x7F+\\u0080-\\uFFFF', 267 ], 268 [ 269 'QWERTY\\x66-\\xFD+', 270 'QWERTYf-\\x7F+\\u0080-\\uFFFF', 271 ], 272 [ 273 'QWERTYf-y+', 274 'QWERTYf-y+', 275 ], 276 [ 277 'QWERTYf-\\x80+', 278 'QWERTYf-\\x7F+\\u0080-\\uFFFF', 279 ], 280 [ 281 'QWERTY\\x66-\\x80+\\x23', 282 'QWERTYf-\\x7F+#\\u0080-\\uFFFF', 283 ], 284 [ 285 'QWERTY\\x66-\\x80+\\xD3', 286 'QWERTYf-\\x7F+\\u0080-\\uFFFF', 287 ], 288 [ 289 '\\\\\\x99', 290 '\\\\\\u0080-\\uFFFF', 291 ], 292 [ 293 '-\\x99', 294 '\\-\\u0080-\\uFFFF', 295 ], 296 [ 297 'QWERTY\\-\\x99', 298 'QWERTY\\-\\u0080-\\uFFFF', 299 ], 300 [ 301 '\\\\x99', 302 '\\\\x99', 303 ], 304 [ 305 'A-\\x9F', 306 'A-\\x7F\\u0080-\\uFFFF', 307 ], 308 [ 309 '\\x66-\\x77QWERTY\\x88-\\x91FXZ', 310 'f-wQWERTYFXZ\\u0080-\\uFFFF', 311 ], 312 [ 313 '\\x66-\\x99QWERTY\\xAA-\\xEEFXZ', 314 'f-\\x7FQWERTYFXZ\\u0080-\\uFFFF', 315 ], 316 ]; 317 } 318 319 /** 320 * @dataProvider provideConvertByteClassToUnicodeClass 321 * @covers Title::convertByteClassToUnicodeClass 322 */ 323 public function testConvertByteClassToUnicodeClass( $byteClass, $unicodeClass ) { 324 $this->assertEquals( $unicodeClass, Title::convertByteClassToUnicodeClass( $byteClass ) ); 325 } 326 327 /** 328 * @dataProvider provideSpecialNamesWithAndWithoutParameter 329 * @covers Title::fixSpecialName 330 */ 331 public function testFixSpecialNameRetainsParameter( $text, $expectedParam ) { 332 $title = Title::newFromText( $text ); 333 $fixed = $title->fixSpecialName(); 334 $stuff = explode( '/', $fixed->getDBkey(), 2 ); 335 if ( count( $stuff ) == 2 ) { 336 $par = $stuff[1]; 337 } else { 338 $par = null; 339 } 340 $this->assertEquals( 341 $expectedParam, 342 $par, 343 "T33100 regression check: Title->fixSpecialName() should preserve parameter" 344 ); 345 } 346 347 public static function provideSpecialNamesWithAndWithoutParameter() { 348 return [ 349 [ 'Special:Version', null ], 350 [ 'Special:Version/', '' ], 351 [ 'Special:Version/param', 'param' ], 352 ]; 353 } 354 355 public function flattenErrorsArray( $errors ) { 356 $result = []; 357 foreach ( $errors as $error ) { 358 $result[] = $error[0]; 359 } 360 361 return $result; 362 } 363 364 /** 365 * @dataProvider provideGetPageViewLanguage 366 * @covers Title::getPageViewLanguage 367 */ 368 public function testGetPageViewLanguage( $expected, $titleText, $contLang, 369 $lang, $variant, $msg = '' 370 ) { 371 // Setup environnement for this test 372 $this->setMwGlobals( [ 373 'wgDefaultLanguageVariant' => $variant, 374 'wgAllowUserJs' => true, 375 ] ); 376 $this->setUserLang( $lang ); 377 $this->setContentLang( $contLang ); 378 379 $title = Title::newFromText( $titleText ); 380 $this->assertInstanceOf( Title::class, $title, 381 "Test must be passed a valid title text, you gave '$titleText'" 382 ); 383 $this->assertEquals( $expected, 384 $title->getPageViewLanguage()->getCode(), 385 $msg 386 ); 387 } 388 389 public static function provideGetPageViewLanguage() { 390 # Format: 391 # - expected 392 # - Title name 393 # - content language (expected in most cases) 394 # - wgLang (on some specific pages) 395 # - wgDefaultLanguageVariant 396 return [ 397 [ 'fr', 'Help:I_need_somebody', 'fr', 'fr', false ], 398 [ 'es', 'Help:I_need_somebody', 'es', 'zh-tw', false ], 399 [ 'zh', 'Help:I_need_somebody', 'zh', 'zh-tw', false ], 400 401 [ 'es', 'Help:I_need_somebody', 'es', 'zh-tw', 'zh-cn' ], 402 [ 'es', 'MediaWiki:About', 'es', 'zh-tw', 'zh-cn' ], 403 [ 'es', 'MediaWiki:About/', 'es', 'zh-tw', 'zh-cn' ], 404 [ 'de', 'MediaWiki:About/de', 'es', 'zh-tw', 'zh-cn' ], 405 [ 'en', 'MediaWiki:Common.js', 'es', 'zh-tw', 'zh-cn' ], 406 [ 'en', 'MediaWiki:Common.css', 'es', 'zh-tw', 'zh-cn' ], 407 [ 'en', 'User:JohnDoe/Common.js', 'es', 'zh-tw', 'zh-cn' ], 408 [ 'en', 'User:JohnDoe/Monobook.css', 'es', 'zh-tw', 'zh-cn' ], 409 410 [ 'zh-cn', 'Help:I_need_somebody', 'zh', 'zh-tw', 'zh-cn' ], 411 [ 'zh', 'MediaWiki:About', 'zh', 'zh-tw', 'zh-cn' ], 412 [ 'zh', 'MediaWiki:About/', 'zh', 'zh-tw', 'zh-cn' ], 413 [ 'de', 'MediaWiki:About/de', 'zh', 'zh-tw', 'zh-cn' ], 414 [ 'zh-cn', 'MediaWiki:About/zh-cn', 'zh', 'zh-tw', 'zh-cn' ], 415 [ 'zh-tw', 'MediaWiki:About/zh-tw', 'zh', 'zh-tw', 'zh-cn' ], 416 [ 'en', 'MediaWiki:Common.js', 'zh', 'zh-tw', 'zh-cn' ], 417 [ 'en', 'MediaWiki:Common.css', 'zh', 'zh-tw', 'zh-cn' ], 418 [ 'en', 'User:JohnDoe/Common.js', 'zh', 'zh-tw', 'zh-cn' ], 419 [ 'en', 'User:JohnDoe/Monobook.css', 'zh', 'zh-tw', 'zh-cn' ], 420 421 [ 'zh-tw', 'Special:NewPages', 'es', 'zh-tw', 'zh-cn' ], 422 [ 'zh-tw', 'Special:NewPages', 'zh', 'zh-tw', 'zh-cn' ], 423 424 ]; 425 } 426 427 /** 428 * @dataProvider provideBaseTitleCases 429 * @covers Title::getBaseText 430 */ 431 public function testGetBaseText( $title, $expected ) { 432 $title = Title::newFromText( $title ); 433 $this->assertSame( $expected, $title->getBaseText() ); 434 } 435 436 /** 437 * @dataProvider provideBaseTitleCases 438 * @covers Title::getBaseTitle 439 */ 440 public function testGetBaseTitle( $title, $expected ) { 441 $title = Title::newFromText( $title ); 442 $base = $title->getBaseTitle(); 443 $this->assertTrue( $base->isValid() ); 444 $this->assertTrue( 445 $base->equals( Title::makeTitleSafe( $title->getNamespace(), $expected ) ) 446 ); 447 } 448 449 public static function provideBaseTitleCases() { 450 return [ 451 # Title, expected base 452 [ 'User:John_Doe', 'John Doe' ], 453 [ 'User:John_Doe/subOne/subTwo', 'John Doe/subOne' ], 454 [ 'User:Foo / Bar / Baz', 'Foo / Bar ' ], 455 [ 'User:Foo/', 'Foo' ], 456 [ 'User:Foo/Bar/', 'Foo/Bar' ], 457 [ 'User:/', '/' ], 458 [ 'User://', '/' ], 459 [ 'User:/oops/', '/oops' ], 460 [ 'User:/indeed', '/indeed' ], 461 [ 'User://indeed', '/' ], 462 [ 'User:/Ramba/Zamba/Mamba/', '/Ramba/Zamba/Mamba' ], 463 [ 'User://x//y//z//', '//x//y//z/' ], 464 ]; 465 } 466 467 /** 468 * @dataProvider provideRootTitleCases 469 * @covers Title::getRootText 470 */ 471 public function testGetRootText( $title, $expected ) { 472 $title = Title::newFromText( $title ); 473 $this->assertEquals( $expected, $title->getRootText() ); 474 } 475 476 /** 477 * @dataProvider provideRootTitleCases 478 * @covers Title::getRootTitle 479 */ 480 public function testGetRootTitle( $title, $expected ) { 481 $title = Title::newFromText( $title ); 482 $root = $title->getRootTitle(); 483 $this->assertTrue( $root->isValid() ); 484 $this->assertTrue( 485 $root->equals( Title::makeTitleSafe( $title->getNamespace(), $expected ) ) 486 ); 487 } 488 489 public static function provideRootTitleCases() { 490 return [ 491 # Title, expected base 492 [ 'User:John_Doe', 'John Doe' ], 493 [ 'User:John_Doe/subOne/subTwo', 'John Doe' ], 494 [ 'User:Foo / Bar / Baz', 'Foo ' ], 495 [ 'User:Foo/', 'Foo' ], 496 [ 'User:Foo/Bar/', 'Foo' ], 497 [ 'User:/', '/' ], 498 [ 'User://', '/' ], 499 [ 'User:/oops/', '/oops' ], 500 [ 'User:/Ramba/Zamba/Mamba/', '/Ramba' ], 501 [ 'User://x//y//z//', '//x' ], 502 [ 'Talk:////', '///' ], 503 [ 'Template:////', '///' ], 504 [ 'Template:Foo////', 'Foo' ], 505 [ 'Template:Foo////Bar', 'Foo' ], 506 ]; 507 } 508 509 /** 510 * @todo Handle $wgNamespacesWithSubpages cases 511 * @dataProvider provideSubpageTitleCases 512 * @covers Title::getSubpageText 513 */ 514 public function testGetSubpageText( $title, $expected ) { 515 $title = Title::newFromText( $title ); 516 $this->assertEquals( $expected, $title->getSubpageText() ); 517 } 518 519 public static function provideSubpageTitleCases() { 520 return [ 521 # Title, expected base 522 [ 'User:John_Doe', 'John Doe' ], 523 [ 'User:John_Doe/subOne/subTwo', 'subTwo' ], 524 [ 'User:John_Doe/subOne', 'subOne' ], 525 [ 'User:/', '/' ], 526 [ 'User://', '' ], 527 [ 'User:/oops/', '' ], 528 [ 'User:/indeed', '/indeed' ], 529 [ 'User://indeed', 'indeed' ], 530 [ 'User:/Ramba/Zamba/Mamba/', '' ], 531 [ 'User://x//y//z//', '' ], 532 [ 'Template:Foo', 'Foo' ] 533 ]; 534 } 535 536 public function provideSubpage() { 537 // NOTE: avoid constructing Title objects in the provider, since it may access the database. 538 return [ 539 [ 'Foo', 'x', new TitleValue( NS_MAIN, 'Foo/x' ) ], 540 [ 'Foo#bar', 'x', new TitleValue( NS_MAIN, 'Foo/x' ) ], 541 [ 'User:Foo', 'x', new TitleValue( NS_USER, 'Foo/x' ) ], 542 [ 'wiki:User:Foo', 'x', new TitleValue( NS_MAIN, 'User:Foo/x', '', 'wiki' ) ], 543 ]; 544 } 545 546 /** 547 * @dataProvider provideSubpage 548 * @covers Title::getSubpage 549 */ 550 public function testSubpage( $title, $sub, LinkTarget $expected ) { 551 $interwikiLookup = $this->createMock( InterwikiLookup::class ); 552 $interwikiLookup->expects( $this->any() ) 553 ->method( 'isValidInterwiki' ) 554 ->willReturnCallback( 555 static function ( $prefix ) { 556 return $prefix == 'wiki'; 557 } 558 ); 559 560 $this->setService( 'InterwikiLookup', $interwikiLookup ); 561 562 $title = Title::newFromText( $title ); 563 $expected = Title::newFromLinkTarget( $expected ); 564 $actual = $title->getSubpage( $sub ); 565 566 // NOTE: convert to string for comparison 567 $this->assertSame( $expected->getPrefixedText(), $actual->getPrefixedText(), 'text form' ); 568 $this->assertTrue( $expected->equals( $actual ), 'Title equality' ); 569 } 570 571 public function provideCastFromPageIdentity() { 572 yield [ null ]; 573 574 $fake = $this->createMock( PageIdentity::class ); 575 $fake->method( 'getId' )->willReturn( 7 ); 576 $fake->method( 'getNamespace' )->willReturn( NS_MAIN ); 577 $fake->method( 'getDBkey' )->willReturn( 'Test' ); 578 579 yield [ $fake ]; 580 581 $fake = $this->createMock( Title::class ); 582 $fake->method( 'getId' )->willReturn( 7 ); 583 $fake->method( 'getNamespace' )->willReturn( NS_MAIN ); 584 $fake->method( 'getDBkey' )->willReturn( 'Test' ); 585 586 yield [ $fake ]; 587 } 588 589 /** 590 * @covers Title::castFromPageIdentity 591 * @dataProvider provideCastFromPageIdentity 592 */ 593 public function testCastFromPageIdentity( ?PageIdentity $value ) { 594 $title = Title::castFromPageIdentity( $value ); 595 596 if ( $value === null ) { 597 $this->assertNull( $title ); 598 } elseif ( $value instanceof Title ) { 599 $this->assertSame( $value, $title ); 600 } else { 601 $this->assertSame( $value->getId(), $title->getArticleID() ); 602 $this->assertSame( $value->getNamespace(), $title->getNamespace() ); 603 $this->assertSame( $value->getDBkey(), $title->getDBkey() ); 604 } 605 } 606 607 public static function provideNewFromTitleValue() { 608 return [ 609 [ new TitleValue( NS_MAIN, 'Foo' ) ], 610 [ new TitleValue( NS_MAIN, 'Foo', 'bar' ) ], 611 [ new TitleValue( NS_USER, 'Hansi_Maier' ) ], 612 ]; 613 } 614 615 /** 616 * @covers Title::newFromTitleValue 617 * @dataProvider provideNewFromTitleValue 618 */ 619 public function testNewFromTitleValue( TitleValue $value ) { 620 $title = Title::newFromTitleValue( $value ); 621 622 $dbkey = str_replace( ' ', '_', $value->getText() ); 623 $this->assertEquals( $dbkey, $title->getDBkey() ); 624 $this->assertEquals( $value->getNamespace(), $title->getNamespace() ); 625 $this->assertEquals( $value->getFragment(), $title->getFragment() ); 626 } 627 628 /** 629 * @covers Title::newFromLinkTarget 630 * @dataProvider provideNewFromTitleValue 631 */ 632 public function testNewFromLinkTarget( LinkTarget $value ) { 633 $title = Title::newFromLinkTarget( $value ); 634 635 $dbkey = str_replace( ' ', '_', $value->getText() ); 636 $this->assertEquals( $dbkey, $title->getDBkey() ); 637 $this->assertEquals( $value->getNamespace(), $title->getNamespace() ); 638 $this->assertEquals( $value->getFragment(), $title->getFragment() ); 639 } 640 641 /** 642 * @covers Title::newFromLinkTarget 643 */ 644 public function testNewFromLinkTarget_clone() { 645 $title = Title::newFromText( __METHOD__ ); 646 $this->assertSame( $title, Title::newFromLinkTarget( $title ) ); 647 648 // The Title::NEW_CLONE flag should ensure that a fresh instance is returned. 649 $clone = Title::newFromLinkTarget( $title, Title::NEW_CLONE ); 650 $this->assertNotSame( $title, $clone ); 651 $this->assertTrue( $clone->equals( $title ) ); 652 } 653 654 public function provideCastFromLinkTarget() { 655 return array_merge( [ [ null ] ], self::provideNewFromTitleValue() ); 656 } 657 658 /** 659 * @covers Title::castFromLinkTarget 660 * @dataProvider provideCastFromLinkTarget 661 */ 662 public function testCastFromLinkTarget( $value ) { 663 $title = Title::castFromLinkTarget( $value ); 664 665 if ( $value === null ) { 666 $this->assertNull( $title ); 667 } else { 668 $dbkey = str_replace( ' ', '_', $value->getText() ); 669 $this->assertSame( $dbkey, $title->getDBkey() ); 670 $this->assertSame( $value->getNamespace(), $title->getNamespace() ); 671 $this->assertSame( $value->getFragment(), $title->getFragment() ); 672 } 673 } 674 675 public static function provideGetTitleValue() { 676 return [ 677 [ 'Foo' ], 678 [ 'Foo#bar' ], 679 [ 'User:Hansi_Maier' ], 680 ]; 681 } 682 683 /** 684 * @covers Title::getTitleValue 685 * @dataProvider provideGetTitleValue 686 */ 687 public function testGetTitleValue( $text ) { 688 $title = Title::newFromText( $text ); 689 $value = $title->getTitleValue(); 690 691 $dbkey = str_replace( ' ', '_', $value->getText() ); 692 $this->assertEquals( $title->getDBkey(), $dbkey ); 693 $this->assertEquals( $title->getNamespace(), $value->getNamespace() ); 694 $this->assertEquals( $title->getFragment(), $value->getFragment() ); 695 } 696 697 public static function provideGetFragment() { 698 return [ 699 [ 'Foo', '' ], 700 [ 'Foo#bar', 'bar' ], 701 [ 'Foo#bär', 'bär' ], 702 703 // Inner whitespace is normalized 704 [ 'Foo#bar_bar', 'bar bar' ], 705 [ 'Foo#bar bar', 'bar bar' ], 706 [ 'Foo#bar bar', 'bar bar' ], 707 708 // Leading whitespace is kept, trailing whitespace is trimmed. 709 // XXX: Is this really want we want? 710 [ 'Foo#_bar_bar_', ' bar bar' ], 711 [ 'Foo# bar bar ', ' bar bar' ], 712 ]; 713 } 714 715 /** 716 * @covers Title::getFragment 717 * @dataProvider provideGetFragment 718 * 719 * @param string $full 720 * @param string $fragment 721 */ 722 public function testGetFragment( $full, $fragment ) { 723 $title = Title::newFromText( $full ); 724 $this->assertEquals( $fragment, $title->getFragment() ); 725 } 726 727 /** 728 * @covers Title::isAlwaysKnown 729 * @dataProvider provideIsAlwaysKnown 730 * @param string $page 731 * @param bool $isKnown 732 */ 733 public function testIsAlwaysKnown( $page, $isKnown ) { 734 $title = Title::newFromText( $page ); 735 $this->assertEquals( $isKnown, $title->isAlwaysKnown() ); 736 } 737 738 public static function provideIsAlwaysKnown() { 739 return [ 740 [ 'Some nonexistent page', false ], 741 [ 'UTPage', false ], 742 [ '#test', true ], 743 [ 'Special:BlankPage', true ], 744 [ 'Special:SomeNonexistentSpecialPage', false ], 745 [ 'MediaWiki:Parentheses', true ], 746 [ 'MediaWiki:Some nonexistent message', false ], 747 ]; 748 } 749 750 /** 751 * @covers Title::isValid 752 * @dataProvider provideIsValid 753 * @param Title $title 754 * @param bool $isValid 755 */ 756 public function testIsValid( Title $title, $isValid ) { 757 $iwLookup = $this->createMock( InterwikiLookup::class ); 758 $iwLookup->method( 'isValidInterwiki' ) 759 ->willReturn( true ); 760 761 $this->setService( 762 'InterwikiLookup', 763 $iwLookup 764 ); 765 766 $this->assertEquals( $isValid, $title->isValid(), $title->getFullText() ); 767 } 768 769 public static function provideIsValid() { 770 return [ 771 [ Title::makeTitle( NS_MAIN, '' ), false ], 772 [ Title::makeTitle( NS_MAIN, '<>' ), false ], 773 [ Title::makeTitle( NS_MAIN, '|' ), false ], 774 [ Title::makeTitle( NS_MAIN, '#' ), false ], 775 [ Title::makeTitle( NS_PROJECT, '#' ), false ], 776 [ Title::makeTitle( NS_MAIN, '', 'test' ), true ], 777 [ Title::makeTitle( NS_PROJECT, '#test' ), false ], 778 [ Title::makeTitle( NS_MAIN, '', 'test', 'wikipedia' ), true ], 779 [ Title::makeTitle( NS_MAIN, 'Test', '', 'wikipedia' ), true ], 780 [ Title::makeTitle( NS_MAIN, 'Test' ), true ], 781 [ Title::makeTitle( NS_SPECIAL, 'Test' ), true ], 782 [ Title::makeTitle( NS_MAIN, ' Test' ), false ], 783 [ Title::makeTitle( NS_MAIN, '_Test' ), false ], 784 [ Title::makeTitle( NS_MAIN, 'Test ' ), false ], 785 [ Title::makeTitle( NS_MAIN, 'Test_' ), false ], 786 [ Title::makeTitle( NS_MAIN, "Test\nthis" ), false ], 787 [ Title::makeTitle( NS_MAIN, "Test\tthis" ), false ], 788 [ Title::makeTitle( -33, 'Test' ), false ], 789 [ Title::makeTitle( 77663399, 'Test' ), false ], 790 ]; 791 } 792 793 /** 794 * @covers Title::isValidRedirectTarget 795 * @dataProvider provideIsValidRedirectTarget 796 * @param Title $title 797 * @param bool $isValid 798 */ 799 public function testIsValidRedirectTarget( Title $title, $isValid ) { 800 $iwLookup = $this->createMock( InterwikiLookup::class ); 801 $iwLookup->method( 'isValidInterwiki' ) 802 ->willReturn( true ); 803 804 $this->setService( 805 'InterwikiLookup', 806 $iwLookup 807 ); 808 809 $this->assertEquals( $isValid, $title->isValidRedirectTarget(), $title->getFullText() ); 810 } 811 812 public static function provideIsValidRedirectTarget() { 813 return [ 814 [ Title::makeTitle( NS_MAIN, '' ), false ], 815 [ Title::makeTitle( NS_MAIN, '', 'test' ), false ], 816 [ Title::makeTitle( NS_MAIN, 'Foo', 'test' ), true ], 817 [ Title::makeTitle( NS_MAIN, '<>' ), false ], 818 [ Title::makeTitle( NS_MAIN, '_' ), false ], 819 [ Title::makeTitle( NS_MAIN, 'Test', '', 'acme' ), true ], 820 [ Title::makeTitle( NS_SPECIAL, 'UserLogout' ), false ], 821 [ Title::makeTitle( NS_SPECIAL, 'RecentChanges' ), true ], 822 ]; 823 } 824 825 /** 826 * @covers Title::canExist 827 * @dataProvider provideCanExist 828 * @param Title $title 829 * @param bool $canExist 830 */ 831 public function testCanExist( Title $title, $canExist ) { 832 $this->assertEquals( $canExist, $title->canExist(), $title->getFullText() ); 833 } 834 835 public static function provideCanExist() { 836 return [ 837 [ Title::makeTitle( NS_MAIN, '' ), false ], 838 [ Title::makeTitle( NS_MAIN, '<>' ), false ], 839 [ Title::makeTitle( NS_MAIN, '|' ), false ], 840 [ Title::makeTitle( NS_MAIN, '#' ), false ], 841 [ Title::makeTitle( NS_PROJECT, '#test' ), false ], 842 [ Title::makeTitle( NS_MAIN, '', 'test', 'acme' ), false ], 843 [ Title::makeTitle( NS_MAIN, 'Test' ), true ], 844 [ Title::makeTitle( NS_MAIN, ' Test' ), false ], 845 [ Title::makeTitle( NS_MAIN, '_Test' ), false ], 846 [ Title::makeTitle( NS_MAIN, 'Test ' ), false ], 847 [ Title::makeTitle( NS_MAIN, 'Test_' ), false ], 848 [ Title::makeTitle( NS_MAIN, "Test\nthis" ), false ], 849 [ Title::makeTitle( NS_MAIN, "Test\tthis" ), false ], 850 [ Title::makeTitle( -33, 'Test' ), false ], 851 [ Title::makeTitle( 77663399, 'Test' ), false ], 852 853 // Valid but can't exist 854 [ Title::makeTitle( NS_MAIN, '', 'test' ), false ], 855 [ Title::makeTitle( NS_SPECIAL, 'Test' ), false ], 856 [ Title::makeTitle( NS_MAIN, 'Test', '', 'acme' ), false ], 857 ]; 858 } 859 860 /** 861 * @covers Title::isAlwaysKnown 862 */ 863 public function testIsAlwaysKnownOnInterwiki() { 864 $title = Title::makeTitle( NS_MAIN, 'Interwiki link', '', 'externalwiki' ); 865 $this->assertTrue( $title->isAlwaysKnown() ); 866 } 867 868 /** 869 * @covers Title::exists 870 */ 871 public function testExists() { 872 $title = Title::makeTitle( NS_PROJECT, 'New page' ); 873 $linkCache = MediaWikiServices::getInstance()->getLinkCache(); 874 875 $article = new Article( $title ); 876 $page = $article->getPage(); 877 $page->doEditContent( new WikitextContent( 'Some [[link]]' ), 'summary' ); 878 879 // Tell Title it doesn't know whether it exists 880 $title->mArticleID = -1; 881 882 // Tell the link cache it doesn't exist when it really does 883 $linkCache->clearLink( $title ); 884 $linkCache->addBadLinkObj( $title ); 885 886 $this->assertFalse( 887 $title->exists(), 888 'exists() should rely on link cache unless READ_LATEST is used' 889 ); 890 $this->assertTrue( 891 $title->exists( Title::READ_LATEST ), 892 'exists() should re-query database when READ_LATEST is used' 893 ); 894 } 895 896 /** 897 * @covers Title::getArticleID 898 * @covers Title::getId 899 */ 900 public function testGetArticleID() { 901 $title = Title::makeTitle( NS_PROJECT, __METHOD__ ); 902 $this->assertSame( 0, $title->getArticleID() ); 903 $this->assertSame( $title->getArticleID(), $title->getId() ); 904 905 $article = new Article( $title ); 906 $page = $article->getPage(); 907 $page->doEditContent( new WikitextContent( 'Some [[link]]' ), 'summary' ); 908 909 $this->assertGreaterThan( 0, $title->getArticleID() ); 910 $this->assertSame( $title->getArticleID(), $title->getId() ); 911 } 912 913 public function provideCanHaveTalkPage() { 914 return [ 915 'User page has talk page' => [ 916 Title::makeTitle( NS_USER, 'Jane' ), true 917 ], 918 'Talke page has talk page' => [ 919 Title::makeTitle( NS_TALK, 'Foo' ), true 920 ], 921 'Special page cannot have talk page' => [ 922 Title::makeTitle( NS_SPECIAL, 'Thing' ), false 923 ], 924 'Virtual namespace cannot have talk page' => [ 925 Title::makeTitle( NS_MEDIA, 'Kitten.jpg' ), false 926 ], 927 'Relative link has no talk page' => [ 928 Title::makeTitle( NS_MAIN, '', 'Kittens' ), false 929 ], 930 'Interwiki link has no talk page' => [ 931 Title::makeTitle( NS_MAIN, 'Kittens', '', 'acme' ), false 932 ], 933 ]; 934 } 935 936 public function provideIsWatchable() { 937 return [ 938 'User page is watchable' => [ 939 Title::makeTitle( NS_USER, 'Jane' ), true 940 ], 941 'Talk page is watchable' => [ 942 Title::makeTitle( NS_TALK, 'Foo' ), true 943 ], 944 'Special page is not watchable' => [ 945 Title::makeTitle( NS_SPECIAL, 'Thing' ), false 946 ], 947 'Virtual namespace is not watchable' => [ 948 Title::makeTitle( NS_MEDIA, 'Kitten.jpg' ), false 949 ], 950 'Relative link is not watchable' => [ 951 Title::makeTitle( NS_MAIN, '', 'Kittens' ), false 952 ], 953 'Interwiki link is not watchable' => [ 954 Title::makeTitle( NS_MAIN, 'Kittens', '', 'acme' ), false 955 ], 956 'Invalid title is not watchable' => [ 957 Title::makeTitle( NS_MAIN, '<' ), false 958 ] 959 ]; 960 } 961 962 public static function provideGetTalkPage_good() { 963 return [ 964 [ Title::makeTitle( NS_MAIN, 'Test' ), Title::makeTitle( NS_TALK, 'Test' ) ], 965 [ Title::makeTitle( NS_TALK, 'Test' ), Title::makeTitle( NS_TALK, 'Test' ) ], 966 ]; 967 } 968 969 public static function provideGetTalkPage_bad() { 970 return [ 971 [ Title::makeTitle( NS_SPECIAL, 'Test' ) ], 972 [ Title::makeTitle( NS_MEDIA, 'Test' ) ], 973 ]; 974 } 975 976 public static function provideGetTalkPage_broken() { 977 // These cases *should* be bad, but are not treated as bad, for backwards compatibility. 978 // See discussion on T227817. 979 return [ 980 [ 981 Title::makeTitle( NS_MAIN, '', 'Kittens' ), 982 Title::makeTitle( NS_TALK, '' ), // Section is lost! 983 false, 984 ], 985 [ 986 Title::makeTitle( NS_MAIN, 'Kittens', '', 'acme' ), 987 Title::makeTitle( NS_TALK, 'Kittens', '' ), // Interwiki prefix is lost! 988 true, 989 ], 990 ]; 991 } 992 993 public static function provideGetSubjectPage_good() { 994 return [ 995 [ Title::makeTitle( NS_TALK, 'Test' ), Title::makeTitle( NS_MAIN, 'Test' ) ], 996 [ Title::makeTitle( NS_MAIN, 'Test' ), Title::makeTitle( NS_MAIN, 'Test' ) ], 997 ]; 998 } 999 1000 public static function provideGetOtherPage_good() { 1001 return [ 1002 [ Title::makeTitle( NS_MAIN, 'Test' ), Title::makeTitle( NS_TALK, 'Test' ) ], 1003 [ Title::makeTitle( NS_TALK, 'Test' ), Title::makeTitle( NS_MAIN, 'Test' ) ], 1004 ]; 1005 } 1006 1007 /** 1008 * @dataProvider provideCanHaveTalkPage 1009 * @covers Title::canHaveTalkPage 1010 * 1011 * @param Title $title 1012 * @param bool $expected 1013 */ 1014 public function testCanHaveTalkPage( Title $title, $expected ) { 1015 $actual = $title->canHaveTalkPage(); 1016 $this->assertSame( $expected, $actual, $title->getPrefixedDBkey() ); 1017 } 1018 1019 /** 1020 * @dataProvider provideIsWatchable 1021 * @covers Title::isWatchable 1022 * 1023 * @param Title $title 1024 * @param bool $expected 1025 */ 1026 public function testIsWatchable( Title $title, $expected ) { 1027 $actual = $title->isWatchable(); 1028 $this->assertSame( $expected, $actual, $title->getPrefixedDBkey() ); 1029 } 1030 1031 /** 1032 * @dataProvider provideGetTalkPage_good 1033 * @covers Title::getTalkPageIfDefined 1034 */ 1035 public function testGetTalkPage_good( Title $title, Title $expected ) { 1036 $actual = $title->getTalkPage(); 1037 $this->assertTrue( $expected->equals( $actual ), $title->getPrefixedDBkey() ); 1038 } 1039 1040 /** 1041 * @dataProvider provideGetTalkPage_bad 1042 * @covers Title::getTalkPageIfDefined 1043 */ 1044 public function testGetTalkPage_bad( Title $title ) { 1045 $this->expectException( MWException::class ); 1046 $title->getTalkPage(); 1047 } 1048 1049 /** 1050 * @dataProvider provideGetTalkPage_broken 1051 * @covers Title::getTalkPageIfDefined 1052 */ 1053 public function testGetTalkPage_broken( Title $title, Title $expected, $valid ) { 1054 $errorLevel = error_reporting( E_ERROR ); 1055 1056 // NOTE: Eventually we want to throw in this case. But while there is still code that 1057 // calls this method without checking, we want to avoid fatal errors. 1058 // See discussion on T227817. 1059 $result = $title->getTalkPage(); 1060 $this->assertTrue( $expected->equals( $result ) ); 1061 $this->assertSame( $valid, $result->isValid() ); 1062 1063 error_reporting( $errorLevel ); 1064 } 1065 1066 /** 1067 * @dataProvider provideGetTalkPage_good 1068 * @covers Title::getTalkPageIfDefined 1069 */ 1070 public function testGetTalkPageIfDefined_good( Title $title, Title $expected ) { 1071 $actual = $title->getTalkPageIfDefined(); 1072 $this->assertNotNull( $actual, $title->getPrefixedDBkey() ); 1073 $this->assertTrue( $expected->equals( $actual ), $title->getPrefixedDBkey() ); 1074 } 1075 1076 /** 1077 * @dataProvider provideGetTalkPage_bad 1078 * @covers Title::getTalkPageIfDefined 1079 */ 1080 public function testGetTalkPageIfDefined_bad( Title $title ) { 1081 $talk = $title->getTalkPageIfDefined(); 1082 $this->assertNull( 1083 $talk, 1084 $title->getPrefixedDBkey() 1085 ); 1086 } 1087 1088 /** 1089 * @dataProvider provideGetSubjectPage_good 1090 * @covers Title::getSubjectPage 1091 */ 1092 public function testGetSubjectPage_good( Title $title, Title $expected ) { 1093 $actual = $title->getSubjectPage(); 1094 $this->assertTrue( $expected->equals( $actual ), $title->getPrefixedDBkey() ); 1095 } 1096 1097 /** 1098 * @dataProvider provideGetOtherPage_good 1099 * @covers Title::getOtherPage 1100 */ 1101 public function testGetOtherPage_good( Title $title, Title $expected ) { 1102 $actual = $title->getOtherPage(); 1103 $this->assertTrue( $expected->equals( $actual ), $title->getPrefixedDBkey() ); 1104 } 1105 1106 /** 1107 * @dataProvider provideGetTalkPage_bad 1108 * @covers Title::getOtherPage 1109 */ 1110 public function testGetOtherPage_bad( Title $title ) { 1111 $this->expectException( MWException::class ); 1112 $title->getOtherPage(); 1113 } 1114 1115 /** 1116 * @dataProvider provideIsMovable 1117 * @covers Title::isMovable 1118 * 1119 * @param string|Title $title 1120 * @param bool $expected 1121 * @param callable|null $hookCallback For TitleIsMovable 1122 */ 1123 public function testIsMovable( $title, $expected, $hookCallback = null ) { 1124 if ( $hookCallback ) { 1125 $this->setTemporaryHook( 'TitleIsMovable', $hookCallback ); 1126 } 1127 if ( is_string( $title ) ) { 1128 $title = Title::newFromText( $title ); 1129 } 1130 1131 $this->assertSame( $expected, $title->isMovable() ); 1132 } 1133 1134 public static function provideIsMovable() { 1135 return [ 1136 'Simple title' => [ 'Foo', true ], 1137 // @todo Should these next two really be true? 1138 'Empty name' => [ Title::makeTitle( NS_MAIN, '' ), true ], 1139 'Invalid name' => [ Title::makeTitle( NS_MAIN, '<' ), true ], 1140 'Interwiki' => [ Title::makeTitle( NS_MAIN, 'Test', '', 'otherwiki' ), false ], 1141 'Special page' => [ 'Special:FooBar', false ], 1142 'Aborted by hook' => [ 'Hooked in place', false, 1143 static function ( Title $title, &$result ) { 1144 $result = false; 1145 } 1146 ], 1147 ]; 1148 } 1149 1150 public function provideCreateFragmentTitle() { 1151 return [ 1152 [ Title::makeTitle( NS_MAIN, 'Test' ), 'foo' ], 1153 [ Title::makeTitle( NS_TALK, 'Test', 'foo' ), '' ], 1154 [ Title::makeTitle( NS_CATEGORY, 'Test', 'foo' ), 'bar' ], 1155 [ Title::makeTitle( NS_MAIN, 'Test1', '', 'interwiki' ), 'baz' ] 1156 ]; 1157 } 1158 1159 /** 1160 * @covers Title::createFragmentTarget 1161 * @dataProvider provideCreateFragmentTitle 1162 */ 1163 public function testCreateFragmentTitle( Title $title, $fragment ) { 1164 $this->setMwGlobals( [ 1165 'wgInterwikiCache' => ClassicInterwikiLookup::buildCdbHash( [ 1166 [ 1167 'iw_prefix' => 'interwiki', 1168 'iw_url' => 'http://example.com/', 1169 'iw_local' => 0, 1170 'iw_trans' => 0, 1171 ], 1172 ] ), 1173 ] ); 1174 1175 $fragmentTitle = $title->createFragmentTarget( $fragment ); 1176 1177 $this->assertEquals( $title->getNamespace(), $fragmentTitle->getNamespace() ); 1178 $this->assertEquals( $title->getText(), $fragmentTitle->getText() ); 1179 $this->assertEquals( $title->getInterwiki(), $fragmentTitle->getInterwiki() ); 1180 $this->assertEquals( $fragment, $fragmentTitle->getFragment() ); 1181 } 1182 1183 public function provideGetPrefixedText() { 1184 return [ 1185 // ns = 0 1186 [ 1187 Title::makeTitle( NS_MAIN, 'Foo bar' ), 1188 'Foo bar' 1189 ], 1190 // ns = 2 1191 [ 1192 Title::makeTitle( NS_USER, 'Foo bar' ), 1193 'User:Foo bar' 1194 ], 1195 // ns = 3 1196 [ 1197 Title::makeTitle( NS_USER_TALK, 'Foo bar' ), 1198 'User talk:Foo bar' 1199 ], 1200 // fragment not included 1201 [ 1202 Title::makeTitle( NS_MAIN, 'Foo bar', 'fragment' ), 1203 'Foo bar' 1204 ], 1205 // ns = -2 1206 [ 1207 Title::makeTitle( NS_MEDIA, 'Foo bar' ), 1208 'Media:Foo bar' 1209 ], 1210 // non-existent namespace 1211 [ 1212 Title::makeTitle( 100777, 'Foo bar' ), 1213 'Special:Badtitle/NS100777:Foo bar' 1214 ], 1215 ]; 1216 } 1217 1218 /** 1219 * @covers Title::getPrefixedText 1220 * @dataProvider provideGetPrefixedText 1221 */ 1222 public function testGetPrefixedText( Title $title, $expected ) { 1223 $this->assertEquals( $expected, $title->getPrefixedText() ); 1224 } 1225 1226 public function provideGetPrefixedDBKey() { 1227 return [ 1228 // ns = 0 1229 [ 1230 Title::makeTitle( NS_MAIN, 'Foo_bar' ), 1231 'Foo_bar' 1232 ], 1233 // ns = 2 1234 [ 1235 Title::makeTitle( NS_USER, 'Foo_bar' ), 1236 'User:Foo_bar' 1237 ], 1238 // ns = 3 1239 [ 1240 Title::makeTitle( NS_USER_TALK, 'Foo_bar' ), 1241 'User_talk:Foo_bar' 1242 ], 1243 // fragment not included 1244 [ 1245 Title::makeTitle( NS_MAIN, 'Foo_bar', 'fragment' ), 1246 'Foo_bar' 1247 ], 1248 // ns = -2 1249 [ 1250 Title::makeTitle( NS_MEDIA, 'Foo_bar' ), 1251 'Media:Foo_bar' 1252 ], 1253 // non-existent namespace 1254 [ 1255 Title::makeTitle( 100777, 'Foo_bar' ), 1256 'Special:Badtitle/NS100777:Foo_bar' 1257 ], 1258 ]; 1259 } 1260 1261 /** 1262 * @covers Title::getPrefixedDBKey 1263 * @dataProvider provideGetPrefixedDBKey 1264 */ 1265 public function testGetPrefixedDBKey( Title $title, $expected ) { 1266 $this->assertEquals( $expected, $title->getPrefixedDBkey() ); 1267 } 1268 1269 /** 1270 * @covers Title::getFragmentForURL 1271 * @dataProvider provideGetFragmentForURL 1272 * 1273 * @param string $titleStr 1274 * @param string $expected 1275 */ 1276 public function testGetFragmentForURL( $titleStr, $expected ) { 1277 $this->setMwGlobals( [ 1278 'wgFragmentMode' => [ 'html5' ], 1279 'wgExternalInterwikiFragmentMode' => 'legacy', 1280 ] ); 1281 $dbw = wfGetDB( DB_MASTER ); 1282 $dbw->insert( 'interwiki', 1283 [ 1284 [ 1285 'iw_prefix' => 'de', 1286 'iw_url' => 'http://de.wikipedia.org/wiki/', 1287 'iw_api' => 'http://de.wikipedia.org/w/api.php', 1288 'iw_wikiid' => 'dewiki', 1289 'iw_local' => 1, 1290 'iw_trans' => 0, 1291 ], 1292 [ 1293 'iw_prefix' => 'zz', 1294 'iw_url' => 'http://zzwiki.org/wiki/', 1295 'iw_api' => 'http://zzwiki.org/w/api.php', 1296 'iw_wikiid' => 'zzwiki', 1297 'iw_local' => 0, 1298 'iw_trans' => 0, 1299 ], 1300 ], 1301 __METHOD__, 1302 [ 'IGNORE' ] 1303 ); 1304 1305 $title = Title::newFromText( $titleStr ); 1306 self::assertEquals( $expected, $title->getFragmentForURL() ); 1307 1308 $dbw->delete( 'interwiki', '*', __METHOD__ ); 1309 } 1310 1311 public function provideGetFragmentForURL() { 1312 return [ 1313 [ 'Foo', '' ], 1314 [ 'Foo#ümlåût', '#ümlåût' ], 1315 [ 'de:Foo#Bå®', '#Bå®' ], 1316 [ 'zz:Foo#тест', '#.D1.82.D0.B5.D1.81.D1.82' ], 1317 ]; 1318 } 1319 1320 /** 1321 * @covers Title::isRawHtmlMessage 1322 * @dataProvider provideIsRawHtmlMessage 1323 */ 1324 public function testIsRawHtmlMessage( $textForm, $expected ) { 1325 $this->setMwGlobals( 'wgRawHtmlMessages', [ 1326 'foobar', 1327 'foo_bar', 1328 'foo-bar', 1329 ] ); 1330 1331 $title = Title::newFromText( $textForm ); 1332 $this->assertSame( $expected, $title->isRawHtmlMessage() ); 1333 } 1334 1335 public function provideIsRawHtmlMessage() { 1336 return [ 1337 [ 'MediaWiki:Foobar', true ], 1338 [ 'MediaWiki:Foo bar', true ], 1339 [ 'MediaWiki:Foo-bar', true ], 1340 [ 'MediaWiki:foo bar', true ], 1341 [ 'MediaWiki:foo-bar', true ], 1342 [ 'MediaWiki:foobar', true ], 1343 [ 'MediaWiki:some-other-message', false ], 1344 [ 'Main Page', false ], 1345 ]; 1346 } 1347 1348 public function provideEquals() { 1349 yield '(newFromText) same text' => [ 1350 Title::newFromText( 'Main Page' ), 1351 Title::newFromText( 'Main Page' ), 1352 true 1353 ]; 1354 yield '(newFromText) different text' => [ 1355 Title::newFromText( 'Main Page' ), 1356 Title::newFromText( 'Not The Main Page' ), 1357 false 1358 ]; 1359 yield '(newFromText) different namespace, same text' => [ 1360 Title::newFromText( 'Main Page' ), 1361 Title::newFromText( 'Project:Main Page' ), 1362 false 1363 ]; 1364 yield '(newFromText) namespace alias' => [ 1365 Title::newFromText( 'File:Example.png' ), 1366 Title::newFromText( 'Image:Example.png' ), 1367 true 1368 ]; 1369 yield '(newFromText) same special page' => [ 1370 Title::newFromText( 'Special:Version' ), 1371 Title::newFromText( 'Special:Version' ), 1372 true 1373 ]; 1374 yield '(newFromText) different special page' => [ 1375 Title::newFromText( 'Special:Version' ), 1376 Title::newFromText( 'Special:Recentchanges' ), 1377 false 1378 ]; 1379 yield '(newFromText) compare special and normal page' => [ 1380 Title::newFromText( 'Special:Version' ), 1381 Title::newFromText( 'Main Page' ), 1382 false 1383 ]; 1384 yield '(makeTitle) same text' => [ 1385 Title::makeTitle( NS_MAIN, 'Foo', '', '' ), 1386 Title::makeTitle( NS_MAIN, 'Foo', '', '' ), 1387 true 1388 ]; 1389 yield '(makeTitle) different text' => [ 1390 Title::makeTitle( NS_MAIN, 'Foo', '', '' ), 1391 Title::makeTitle( NS_MAIN, 'Bar', '', '' ), 1392 false 1393 ]; 1394 yield '(makeTitle) different namespace, same text' => [ 1395 Title::makeTitle( NS_MAIN, 'Foo', '', '' ), 1396 Title::makeTitle( NS_TALK, 'Foo', '', '' ), 1397 false 1398 ]; 1399 yield '(makeTitle) same fragment' => [ 1400 Title::makeTitle( NS_MAIN, 'Foo', 'Bar', '' ), 1401 Title::makeTitle( NS_MAIN, 'Foo', 'Bar', '' ), 1402 true 1403 ]; 1404 yield '(makeTitle) different fragment (ignored)' => [ 1405 Title::makeTitle( NS_MAIN, 'Foo', 'Bar', '' ), 1406 Title::makeTitle( NS_MAIN, 'Foo', 'Baz', '' ), 1407 true 1408 ]; 1409 yield '(makeTitle) fragment vs no fragment (ignored)' => [ 1410 Title::makeTitle( NS_MAIN, 'Foo', 'Bar', '' ), 1411 Title::makeTitle( NS_MAIN, 'Foo', '', '' ), 1412 true 1413 ]; 1414 yield '(makeTitle) same interwiki' => [ 1415 Title::makeTitle( NS_MAIN, 'Foo', '', 'baz' ), 1416 Title::makeTitle( NS_MAIN, 'Foo', '', 'baz' ), 1417 true 1418 ]; 1419 yield '(makeTitle) different interwiki' => [ 1420 Title::makeTitle( NS_MAIN, 'Foo', '', '' ), 1421 Title::makeTitle( NS_MAIN, 'Foo', '', 'baz' ), 1422 false 1423 ]; 1424 1425 // Wrong type 1426 yield '(makeTitle vs PageIdentityValue) name text' => [ 1427 Title::makeTitle( NS_MAIN, 'Foo' ), 1428 new PageIdentityValue( 0, NS_MAIN, 'Foo', PageIdentity::LOCAL ), 1429 false 1430 ]; 1431 yield '(makeTitle vs TitleValue) name text' => [ 1432 Title::makeTitle( NS_MAIN, 'Foo' ), 1433 new TitleValue( NS_MAIN, 'Foo' ), 1434 false 1435 ]; 1436 yield '(makeTitle vs UserIdentityValue) name text' => [ 1437 Title::makeTitle( NS_MAIN, 'Foo' ), 1438 new UserIdentityValue( 7, 'Foo' ), 1439 false 1440 ]; 1441 } 1442 1443 /** 1444 * @covers Title::getPreviousRevisionID 1445 * @covers MediaWiki\Revision\RevisionStore::getRelativeRevision 1446 */ 1447 public function testGetPreviousRevisionID_deprecated() { 1448 $this->expectDeprecation(); 1449 Title::makeTitle( NS_MAIN, 'Foo' )->getPreviousRevisionID( 2233 ); 1450 } 1451 1452 /** 1453 * @covers Title::getNextRevisionID 1454 * @covers Title::getRelativeRevisionID 1455 */ 1456 public function testGetNextRevisionID_deprecated() { 1457 $this->expectDeprecation(); 1458 Title::makeTitle( NS_MAIN, 'Foo' )->getNextRevisionID( 123456789 ); 1459 } 1460 1461 /** 1462 * @covers Title::equals 1463 * @dataProvider provideEquals 1464 */ 1465 public function testEquals( Title $firstValue, $secondValue, $expectedSame ) { 1466 $this->assertSame( 1467 $expectedSame, 1468 $firstValue->equals( $secondValue ) 1469 ); 1470 } 1471 1472 public function provideIsSamePageAs() { 1473 $title = Title::makeTitle( 0, 'Foo' ); 1474 $title->resetArticleID( 1 ); 1475 yield '(PageIdentityValue) same text, title has ID 0' => [ 1476 $title, 1477 new PageIdentityValue( 1, 0, 'Foo', PageIdentity::LOCAL ), 1478 true 1479 ]; 1480 1481 $title = Title::makeTitle( 1, 'Bar_Baz' ); 1482 $title->resetArticleID( 0 ); 1483 yield '(PageIdentityValue) same text, PageIdentityValue has ID 0' => [ 1484 $title, 1485 new PageIdentityValue( 0, 1, 'Bar_Baz', PageIdentity::LOCAL ), 1486 true 1487 ]; 1488 1489 $title = Title::makeTitle( 0, 'Foo' ); 1490 $title->resetArticleID( 0 ); 1491 yield '(PageIdentityValue) different text, both IDs are 0' => [ 1492 $title, 1493 new PageIdentityValue( 0, 0, 'Foozz', PageIdentity::LOCAL ), 1494 false 1495 ]; 1496 1497 $title = Title::makeTitle( 0, 'Foo' ); 1498 $title->resetArticleID( 0 ); 1499 yield '(PageIdentityValue) different namespace' => [ 1500 $title, 1501 new PageIdentityValue( 0, 1, 'Foo', PageIdentity::LOCAL ), 1502 false 1503 ]; 1504 1505 $title = Title::makeTitle( 0, 'Foo', '' ); 1506 $title->resetArticleID( 1 ); 1507 yield '(PageIdentityValue) different wiki, different ID' => [ 1508 $title, 1509 new PageIdentityValue( 1, 0, 'Foo', 'bar' ), 1510 false 1511 ]; 1512 1513 $title = Title::makeTitle( 0, 'Foo', '' ); 1514 $title->resetArticleID( 0 ); 1515 yield '(PageIdentityValue) different wiki, both IDs are 0' => [ 1516 $title, 1517 new PageIdentityValue( 0, 0, 'Foo', 'bar' ), 1518 false 1519 ]; 1520 } 1521 1522 /** 1523 * @covers Title::isSamePageAs 1524 * @dataProvider provideIsSamePageAs 1525 */ 1526 public function testIsSamePageAs( Title $firstValue, $secondValue, $expectedSame ) { 1527 $this->assertSame( 1528 $expectedSame, 1529 $firstValue->isSamePageAs( $secondValue ) 1530 ); 1531 } 1532 1533 public function provideIsSameLinkAs() { 1534 yield 'same text' => [ 1535 Title::makeTitle( 0, 'Foo' ), 1536 new TitleValue( 0, 'Foo' ), 1537 true 1538 ]; 1539 yield 'same namespace' => [ 1540 Title::makeTitle( 1, 'Bar_Baz' ), 1541 new TitleValue( 1, 'Bar_Baz' ), 1542 true 1543 ]; 1544 yield 'same text, different namespace' => [ 1545 Title::makeTitle( 0, 'Foo' ), 1546 new TitleValue( 1, 'Foo' ), 1547 false 1548 ]; 1549 yield 'different text' => [ 1550 Title::makeTitle( 0, 'Foo' ), 1551 new TitleValue( 0, 'Foozz' ), 1552 false 1553 ]; 1554 yield 'different fragment' => [ 1555 Title::makeTitle( 0, 'Foo', '' ), 1556 new TitleValue( 0, 'Foo', 'Bar' ), 1557 false 1558 ]; 1559 yield 'different interwiki' => [ 1560 Title::makeTitle( 0, 'Foo', '', 'bar' ), 1561 new TitleValue( 0, 'Foo', '', '' ), 1562 false 1563 ]; 1564 } 1565 1566 /** 1567 * @covers Title::isSameLinkAs 1568 * @dataProvider provideIsSameLinkAs 1569 */ 1570 public function testIsSameLinkAs( Title $firstValue, $secondValue, $expectedSame ) { 1571 $this->assertSame( 1572 $expectedSame, 1573 $firstValue->isSameLinkAs( $secondValue ) 1574 ); 1575 } 1576 1577 /** 1578 * @covers Title::newMainPage 1579 */ 1580 public function testNewMainPage() { 1581 $mock = $this->createMock( MessageCache::class ); 1582 $mock->method( 'get' )->willReturn( 'Foresheet' ); 1583 $mock->method( 'transform' )->willReturn( 'Foresheet' ); 1584 1585 $this->setService( 'MessageCache', $mock ); 1586 1587 $this->assertSame( 1588 'Foresheet', 1589 Title::newMainPage()->getText() 1590 ); 1591 } 1592 1593 /** 1594 * @covers Title::newMainPage 1595 */ 1596 public function testNewMainPageWithLocal() { 1597 $local = $this->createMock( MessageLocalizer::class ); 1598 $local->method( 'msg' )->willReturn( new RawMessage( 'Prime Article' ) ); 1599 1600 $this->assertSame( 1601 'Prime Article', 1602 Title::newMainPage( $local )->getText() 1603 ); 1604 } 1605 1606 /** 1607 * @covers Title::loadRestrictions 1608 */ 1609 public function testLoadRestrictions() { 1610 $title = Title::newFromText( 'UTPage1' ); 1611 $title->loadRestrictions(); 1612 $this->assertTrue( $title->areRestrictionsLoaded() ); 1613 $title = $this->getExistingTestPage( 'UTest1' )->getTitle(); 1614 $title->loadRestrictions(); 1615 $this->assertTrue( $title->areRestrictionsLoaded() ); 1616 $this->assertEquals( 1617 $title->getRestrictionExpiry( 'create' ), 1618 'infinity' 1619 ); 1620 $page = $this->getNonexistingTestPage( 'UTest1' ); 1621 $title = $page->getTitle(); 1622 $protectExpiry = wfTimestamp( TS_MW, time() + 10000 ); 1623 $cascade = 0; 1624 $page->doUpdateRestrictions( 1625 [ 'create' => 'sysop' ], 1626 [ 'create' => $protectExpiry ], 1627 $cascade, 1628 'test', 1629 $this->getTestSysop()->getUser() 1630 ); 1631 $title->mRestrictionsLoaded = false; 1632 $title->loadRestrictions(); 1633 $this->assertSame( 1634 $title->getRestrictionExpiry( 'create' ), 1635 $protectExpiry 1636 ); 1637 } 1638 1639 public function provideRestrictionsRows() { 1640 yield [ [ (object)[ 1641 'pr_id' => 1, 1642 'pr_page' => 1, 1643 'pr_type' => 'edit', 1644 'pr_level' => 'sysop', 1645 'pr_cascade' => 0, 1646 'pr_user' => null, 1647 'pr_expiry' => 'infinity' 1648 ] ] ]; 1649 yield [ [ (object)[ 1650 'pr_id' => 1, 1651 'pr_page' => 1, 1652 'pr_type' => 'edit', 1653 'pr_level' => 'sysop', 1654 'pr_cascade' => 0, 1655 'pr_user' => null, 1656 'pr_expiry' => 'infinity' 1657 ] ] ]; 1658 yield [ [ (object)[ 1659 'pr_id' => 1, 1660 'pr_page' => 1, 1661 'pr_type' => 'move', 1662 'pr_level' => 'sysop', 1663 'pr_cascade' => 0, 1664 'pr_user' => null, 1665 'pr_expiry' => wfTimestamp( TS_MW, time() + 10000 ) 1666 ] ] ]; 1667 } 1668 1669 /** 1670 * @covers Title::loadRestrictionsFromRows 1671 * @dataProvider provideRestrictionsRows 1672 */ 1673 public function testloadRestrictionsFromRows( $rows ) { 1674 $title = $this->getExistingTestPage( 'UTest1' )->getTitle(); 1675 $title->loadRestrictionsFromRows( $rows ); 1676 $this->assertSame( 1677 $rows[0]->pr_level, 1678 $title->getRestrictions( $rows[0]->pr_type )[0] 1679 ); 1680 $this->assertSame( 1681 $rows[0]->pr_expiry, 1682 $title->getRestrictionExpiry( $rows[0]->pr_type ) 1683 ); 1684 } 1685 1686 /** 1687 * @covers Title::getRestrictions 1688 */ 1689 public function testGetRestrictions() { 1690 $title = $this->getExistingTestPage( 'UTest1' )->getTitle(); 1691 $title->mRestrictions = [ 1692 'a' => [ 'sysop' ], 1693 'b' => [ 'sysop' ], 1694 'c' => [ 'sysop' ] 1695 ]; 1696 $title->mRestrictionsLoaded = true; 1697 $this->assertArrayEquals( [ 'sysop' ], $title->getRestrictions( 'a' ) ); 1698 $this->assertArrayEquals( [], $title->getRestrictions( 'error' ) ); 1699 // TODO: maybe test if loadRestrictionsFromRows() is called? 1700 } 1701 1702 /** 1703 * @covers Title::getAllRestrictions 1704 */ 1705 public function testGetAllRestrictions() { 1706 $title = $this->getExistingTestPage( 'UTest1' )->getTitle(); 1707 $title->mRestrictions = [ 1708 'a' => [ 'sysop' ], 1709 'b' => [ 'sysop' ], 1710 'c' => [ 'sysop' ] 1711 ]; 1712 $title->mRestrictionsLoaded = true; 1713 $this->assertArrayEquals( 1714 $title->mRestrictions, 1715 $title->getAllRestrictions() 1716 ); 1717 } 1718 1719 /** 1720 * @covers Title::getRestrictionExpiry 1721 */ 1722 public function testGetRestrictionExpiry() { 1723 $title = $this->getExistingTestPage( 'UTest1' )->getTitle(); 1724 $reflection = new ReflectionClass( $title ); 1725 $reflection_property = $reflection->getProperty( 'mRestrictionsExpiry' ); 1726 $reflection_property->setAccessible( true ); 1727 $reflection_property->setValue( $title, [ 1728 'a' => 'infinity', 'b' => 'infinity', 'c' => 'infinity' 1729 ] ); 1730 $title->mRestrictionsLoaded = true; 1731 $this->assertSame( 'infinity', $title->getRestrictionExpiry( 'a' ) ); 1732 $this->assertArrayEquals( [], $title->getRestrictions( 'error' ) ); 1733 } 1734 1735 /** 1736 * @covers Title::getTitleProtection 1737 */ 1738 public function testGetTitleProtection() { 1739 $title = $this->getNonexistingTestPage( 'UTest1' )->getTitle(); 1740 $title->mTitleProtection = false; 1741 $this->assertFalse( $title->getTitleProtection() ); 1742 } 1743 1744 /** 1745 * @covers Title::isSemiProtected 1746 */ 1747 public function testIsSemiProtected() { 1748 $title = $this->getExistingTestPage( 'UTest1' )->getTitle(); 1749 $title->mRestrictions = [ 1750 'edit' => [ 'sysop' ] 1751 ]; 1752 $this->setMwGlobals( [ 1753 'wgSemiprotectedRestrictionLevels' => [ 'autoconfirmed' ], 1754 'wgRestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ] 1755 ] ); 1756 $this->assertFalse( $title->isSemiProtected( 'edit' ) ); 1757 $title->mRestrictions = [ 1758 'edit' => [ 'autoconfirmed' ] 1759 ]; 1760 $this->assertTrue( $title->isSemiProtected( 'edit' ) ); 1761 } 1762 1763 /** 1764 * @covers Title::deleteTitleProtection 1765 */ 1766 public function testDeleteTitleProtection() { 1767 $title = $this->getExistingTestPage( 'UTest1' )->getTitle(); 1768 $this->assertFalse( $title->getTitleProtection() ); 1769 } 1770 1771 /** 1772 * @covers Title::isProtected 1773 */ 1774 public function testIsProtected() { 1775 $title = $this->getExistingTestPage( 'UTest1' )->getTitle(); 1776 $this->setMwGlobals( [ 1777 'wgRestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ], 1778 'wgRestrictionTypes' => [ 'create', 'edit', 'move', 'upload' ] 1779 ] ); 1780 $title->mRestrictions = [ 1781 'edit' => [ 'sysop' ] 1782 ]; 1783 $this->assertFalse( $title->isProtected( 'edit' ) ); 1784 $title->mRestrictions = [ 1785 'edit' => [ 'test' ] 1786 ]; 1787 $this->assertFalse( $title->isProtected( 'edit' ) ); 1788 } 1789 1790 /** 1791 * @covers Title::isNamespaceProtected 1792 */ 1793 public function testIsNamespaceProtected() { 1794 $title = $this->getExistingTestPage( 'UTest1' )->getTitle(); 1795 $this->setMwGlobals( [ 1796 'wgNamespaceProtection' => [] 1797 ] ); 1798 $this->assertFalse( 1799 $title->isNamespaceProtected( $this->getTestUser()->getUser() ) 1800 ); 1801 $this->setMwGlobals( [ 1802 'wgNamespaceProtection' => [ 1803 NS_MAIN => [ 'edit-main' ] 1804 ] 1805 ] ); 1806 $this->assertTrue( 1807 $title->isNamespaceProtected( $this->getTestUser()->getUser() ) 1808 ); 1809 } 1810 1811 /** 1812 * @covers Title::isCascadeProtected 1813 */ 1814 public function testIsCascadeProtected() { 1815 $page = $this->getExistingTestPage( 'UTest1' ); 1816 $title = $page->getTitle(); 1817 $reflection = new ReflectionClass( $title ); 1818 $reflection_property = $reflection->getProperty( 'mHasCascadingRestrictions' ); 1819 $reflection_property->setAccessible( true ); 1820 $reflection_property->setValue( $title, true ); 1821 $this->assertTrue( $title->isCascadeProtected() ); 1822 $reflection_property->setValue( $title, null ); 1823 $this->assertFalse( $title->isCascadeProtected() ); 1824 $reflection_property->setValue( $title, null ); 1825 $cascade = 1; 1826 $anotherPage = $this->getExistingTestPage( 'UTest2' ); 1827 $anotherPage->doEditContent( new WikitextContent( '{{:UTest1}}' ), 'test' ); 1828 $anotherPage->doUpdateRestrictions( 1829 [ 'edit' => 'sysop' ], 1830 [], 1831 $cascade, 1832 'test', 1833 $this->getTestSysop()->getUser() 1834 ); 1835 $this->assertTrue( $title->isCascadeProtected() ); 1836 } 1837 1838 /** 1839 * @covers Title::getCascadeProtectionSources 1840 */ 1841 public function testGetCascadeProtectionSources() { 1842 $page = $this->getExistingTestPage( 'UTest1' ); 1843 $title = $page->getTitle(); 1844 1845 $title->mCascadeSources = []; 1846 $this->assertArrayEquals( 1847 [ [], [] ], 1848 $title->getCascadeProtectionSources( true ) 1849 ); 1850 1851 $reflection = new ReflectionClass( $title ); 1852 $reflection_property = $reflection->getProperty( 'mHasCascadingRestrictions' ); 1853 $reflection_property->setAccessible( true ); 1854 $reflection_property->setValue( $title, true ); 1855 $this->assertArrayEquals( 1856 [ true, [] ], 1857 $title->getCascadeProtectionSources( false ) 1858 ); 1859 1860 $title->mCascadeSources = null; 1861 $reflection_property->setValue( $title, null ); 1862 $this->assertArrayEquals( 1863 [ false, [] ], 1864 $title->getCascadeProtectionSources( false ) 1865 ); 1866 1867 $title->mCascadeSources = null; 1868 $reflection_property->setValue( $title, null ); 1869 $this->assertArrayEquals( 1870 [ [], [] ], 1871 $title->getCascadeProtectionSources( true ) 1872 ); 1873 1874 // TODO: this might partially duplicate testIsCascadeProtected method above 1875 1876 $cascade = 1; 1877 $anotherPage = $this->getExistingTestPage( 'UTest2' ); 1878 $anotherPage->doEditContent( new WikitextContent( '{{:UTest1}}' ), 'test' ); 1879 $anotherPage->doUpdateRestrictions( 1880 [ 'edit' => 'sysop' ], 1881 [], 1882 $cascade, 1883 'test', 1884 $this->getTestSysop()->getUser() 1885 ); 1886 1887 $this->assertArrayEquals( 1888 [ true, [] ], 1889 $title->getCascadeProtectionSources( false ) 1890 ); 1891 1892 $title->mCascadeSources = null; 1893 $result = $title->getCascadeProtectionSources( true ); 1894 $this->assertArrayEquals( 1895 [ 'edit' => [ 'sysop' ] ], 1896 $result[1] 1897 ); 1898 $this->assertArrayHasKey( 1899 $anotherPage->getTitle()->getArticleID(), $result[0] 1900 ); 1901 } 1902 1903 /** 1904 * @covers Title::getCdnUrls 1905 */ 1906 public function testGetCdnUrls() { 1907 $this->assertEquals( 1908 [ 1909 'https://example.org/wiki/Example', 1910 'https://example.org/w/index.php?title=Example&action=history', 1911 ], 1912 Title::makeTitle( NS_MAIN, 'Example' )->getCdnUrls(), 1913 'article' 1914 ); 1915 } 1916 1917 /** 1918 * @covers \MediaWiki\Page\PageStore::getSubpages 1919 */ 1920 public function testGetSubpages() { 1921 $existingPage = $this->getExistingTestPage(); 1922 $title = $existingPage->getTitle(); 1923 1924 $this->setMwGlobals( 'wgNamespacesWithSubpages', [ $title->getNamespace() => true ] ); 1925 1926 $this->getExistingTestPage( $title->getSubpage( 'A' ) ); 1927 $this->getExistingTestPage( $title->getSubpage( 'B' ) ); 1928 1929 $notQuiteSubpageTitle = $title->getPrefixedDBkey() . 'X'; // no slash! 1930 $this->getExistingTestPage( $notQuiteSubpageTitle ); 1931 1932 $subpages = iterator_to_array( $title->getSubpages() ); 1933 1934 $this->assertCount( 2, $subpages ); 1935 $this->assertCount( 1, $title->getSubpages( 1 ) ); 1936 } 1937 1938 /** 1939 * @covers \MediaWiki\Page\PageStore::getSubpages 1940 */ 1941 public function testGetSubpages_disabled() { 1942 $this->setMwGlobals( 'wgNamespacesWithSubpages', [] ); 1943 1944 $existingPage = $this->getExistingTestPage(); 1945 $title = $existingPage->getTitle(); 1946 1947 $this->getExistingTestPage( $title->getSubpage( 'A' ) ); 1948 $this->getExistingTestPage( $title->getSubpage( 'B' ) ); 1949 1950 $this->assertEmpty( $title->getSubpages() ); 1951 } 1952} 1953