diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/TitleTest.php')
-rw-r--r-- | www/wiki/tests/phpunit/includes/TitleTest.php | 968 |
1 files changed, 968 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/TitleTest.php b/www/wiki/tests/phpunit/includes/TitleTest.php new file mode 100644 index 00000000..c81a0787 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/TitleTest.php @@ -0,0 +1,968 @@ +<?php + +/** + * @group Database + * @group Title + */ +class TitleTest extends MediaWikiTestCase { + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( [ + 'wgAllowUserJs' => false, + 'wgDefaultLanguageVariant' => false, + 'wgMetaNamespace' => 'Project', + ] ); + $this->setUserLang( 'en' ); + $this->setContentLang( 'en' ); + } + + /** + * @covers Title::legalChars + */ + public function testLegalChars() { + $titlechars = Title::legalChars(); + + foreach ( range( 1, 255 ) as $num ) { + $chr = chr( $num ); + if ( strpos( "#[]{}<>|", $chr ) !== false || preg_match( "/[\\x00-\\x1f\\x7f]/", $chr ) ) { + $this->assertFalse( + (bool)preg_match( "/[$titlechars]/", $chr ), + "chr($num) = $chr is not a valid titlechar" + ); + } else { + $this->assertTrue( + (bool)preg_match( "/[$titlechars]/", $chr ), + "chr($num) = $chr is a valid titlechar" + ); + } + } + } + + public static function provideValidSecureAndSplit() { + return [ + [ 'Sandbox' ], + [ 'A "B"' ], + [ 'A \'B\'' ], + [ '.com' ], + [ '~' ], + [ '#' ], + [ '"' ], + [ '\'' ], + [ 'Talk:Sandbox' ], + [ 'Talk:Foo:Sandbox' ], + [ 'File:Example.svg' ], + [ 'File_talk:Example.svg' ], + [ 'Foo/.../Sandbox' ], + [ 'Sandbox/...' ], + [ 'A~~' ], + [ ':A' ], + // Length is 256 total, but only title part matters + [ 'Category:' . str_repeat( 'x', 248 ) ], + [ str_repeat( 'x', 252 ) ], + // interwiki prefix + [ 'localtestiw: #anchor' ], + [ 'localtestiw:' ], + [ 'localtestiw:foo' ], + [ 'localtestiw: foo # anchor' ], + [ 'localtestiw: Talk: Sandbox # anchor' ], + [ 'remotetestiw:' ], + [ 'remotetestiw: Talk: # anchor' ], + [ 'remotetestiw: #bar' ], + [ 'remotetestiw: Talk:' ], + [ 'remotetestiw: Talk: Foo' ], + [ 'localtestiw:remotetestiw:' ], + [ 'localtestiw:remotetestiw:foo' ] + ]; + } + + public static function provideInvalidSecureAndSplit() { + return [ + [ '', 'title-invalid-empty' ], + [ ':', 'title-invalid-empty' ], + [ '__ __', 'title-invalid-empty' ], + [ ' __ ', 'title-invalid-empty' ], + // Bad characters forbidden regardless of wgLegalTitleChars + [ 'A [ B', 'title-invalid-characters' ], + [ 'A ] B', 'title-invalid-characters' ], + [ 'A { B', 'title-invalid-characters' ], + [ 'A } B', 'title-invalid-characters' ], + [ 'A < B', 'title-invalid-characters' ], + [ 'A > B', 'title-invalid-characters' ], + [ 'A | B', 'title-invalid-characters' ], + [ "A \t B", 'title-invalid-characters' ], + [ "A \n B", 'title-invalid-characters' ], + // URL encoding + [ 'A%20B', 'title-invalid-characters' ], + [ 'A%23B', 'title-invalid-characters' ], + [ 'A%2523B', 'title-invalid-characters' ], + // XML/HTML character entity references + // Note: Commented out because they are not marked invalid by the PHP test as + // Title::newFromText runs Sanitizer::decodeCharReferencesAndNormalize first. + // 'A é B', + // 'A é B', + // 'A é B', + // Subject of NS_TALK does not roundtrip to NS_MAIN + [ 'Talk:File:Example.svg', 'title-invalid-talk-namespace' ], + // Directory navigation + [ '.', 'title-invalid-relative' ], + [ '..', 'title-invalid-relative' ], + [ './Sandbox', 'title-invalid-relative' ], + [ '../Sandbox', 'title-invalid-relative' ], + [ 'Foo/./Sandbox', 'title-invalid-relative' ], + [ 'Foo/../Sandbox', 'title-invalid-relative' ], + [ 'Sandbox/.', 'title-invalid-relative' ], + [ 'Sandbox/..', 'title-invalid-relative' ], + // Tilde + [ 'A ~~~ Name', 'title-invalid-magic-tilde' ], + [ 'A ~~~~ Signature', 'title-invalid-magic-tilde' ], + [ 'A ~~~~~ Timestamp', 'title-invalid-magic-tilde' ], + // Length + [ str_repeat( 'x', 256 ), 'title-invalid-too-long' ], + // Namespace prefix without actual title + [ 'Talk:', 'title-invalid-empty' ], + [ 'Talk:#', 'title-invalid-empty' ], + [ 'Category: ', 'title-invalid-empty' ], + [ 'Category: #bar', 'title-invalid-empty' ], + // interwiki prefix + [ 'localtestiw: Talk: # anchor', 'title-invalid-empty' ], + [ 'localtestiw: Talk:', 'title-invalid-empty' ] + ]; + } + + private function secureAndSplitGlobals() { + $this->setMwGlobals( [ + 'wgLocalInterwikis' => [ 'localtestiw' ], + 'wgHooks' => [ + 'InterwikiLoadPrefix' => [ + function ( $prefix, &$data ) { + if ( $prefix === 'localtestiw' ) { + $data = [ 'iw_url' => 'localtestiw' ]; + } elseif ( $prefix === 'remotetestiw' ) { + $data = [ 'iw_url' => 'remotetestiw' ]; + } + return false; + } + ] + ] + ] ); + + // Reset TitleParser since we modified $wgLocalInterwikis + $this->setService( 'TitleParser', new MediaWikiTitleCodec( + Language::factory( 'en' ), + new GenderCache(), + [ 'localtestiw' ] + ) ); + } + + /** + * See also mediawiki.Title.test.js + * @covers Title::secureAndSplit + * @dataProvider provideValidSecureAndSplit + * @note This mainly tests MediaWikiTitleCodec::parseTitle(). + */ + public function testSecureAndSplitValid( $text ) { + $this->secureAndSplitGlobals(); + $this->assertInstanceOf( Title::class, Title::newFromText( $text ), "Valid: $text" ); + } + + /** + * See also mediawiki.Title.test.js + * @covers Title::secureAndSplit + * @dataProvider provideInvalidSecureAndSplit + * @note This mainly tests MediaWikiTitleCodec::parseTitle(). + */ + public function testSecureAndSplitInvalid( $text, $expectedErrorMessage ) { + $this->secureAndSplitGlobals(); + try { + Title::newFromTextThrow( $text ); // should throw + $this->assertTrue( false, "Invalid: $text" ); + } catch ( MalformedTitleException $ex ) { + $this->assertEquals( $expectedErrorMessage, $ex->getErrorMessage(), "Invalid: $text" ); + } + } + + public static function provideConvertByteClassToUnicodeClass() { + return [ + [ + ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+', + ' %!"$&\'()*,\\-./0-9:;=?@A-Z\\\\\\^_`a-z~+\\u0080-\\uFFFF', + ], + [ + 'QWERTYf-\\xFF+', + 'QWERTYf-\\x7F+\\u0080-\\uFFFF', + ], + [ + 'QWERTY\\x66-\\xFD+', + 'QWERTYf-\\x7F+\\u0080-\\uFFFF', + ], + [ + 'QWERTYf-y+', + 'QWERTYf-y+', + ], + [ + 'QWERTYf-\\x80+', + 'QWERTYf-\\x7F+\\u0080-\\uFFFF', + ], + [ + 'QWERTY\\x66-\\x80+\\x23', + 'QWERTYf-\\x7F+#\\u0080-\\uFFFF', + ], + [ + 'QWERTY\\x66-\\x80+\\xD3', + 'QWERTYf-\\x7F+\\u0080-\\uFFFF', + ], + [ + '\\\\\\x99', + '\\\\\\u0080-\\uFFFF', + ], + [ + '-\\x99', + '\\-\\u0080-\\uFFFF', + ], + [ + 'QWERTY\\-\\x99', + 'QWERTY\\-\\u0080-\\uFFFF', + ], + [ + '\\\\x99', + '\\\\x99', + ], + [ + 'A-\\x9F', + 'A-\\x7F\\u0080-\\uFFFF', + ], + [ + '\\x66-\\x77QWERTY\\x88-\\x91FXZ', + 'f-wQWERTYFXZ\\u0080-\\uFFFF', + ], + [ + '\\x66-\\x99QWERTY\\xAA-\\xEEFXZ', + 'f-\\x7FQWERTYFXZ\\u0080-\\uFFFF', + ], + ]; + } + + /** + * @dataProvider provideConvertByteClassToUnicodeClass + * @covers Title::convertByteClassToUnicodeClass + */ + public function testConvertByteClassToUnicodeClass( $byteClass, $unicodeClass ) { + $this->assertEquals( $unicodeClass, Title::convertByteClassToUnicodeClass( $byteClass ) ); + } + + /** + * @dataProvider provideSpecialNamesWithAndWithoutParameter + * @covers Title::fixSpecialName + */ + public function testFixSpecialNameRetainsParameter( $text, $expectedParam ) { + $title = Title::newFromText( $text ); + $fixed = $title->fixSpecialName(); + $stuff = explode( '/', $fixed->getDBkey(), 2 ); + if ( count( $stuff ) == 2 ) { + $par = $stuff[1]; + } else { + $par = null; + } + $this->assertEquals( + $expectedParam, + $par, + "T33100 regression check: Title->fixSpecialName() should preserve parameter" + ); + } + + public static function provideSpecialNamesWithAndWithoutParameter() { + return [ + [ 'Special:Version', null ], + [ 'Special:Version/', '' ], + [ 'Special:Version/param', 'param' ], + ]; + } + + /** + * Auth-less test of Title::isValidMoveOperation + * + * @param string $source + * @param string $target + * @param array|string|bool $expected Required error + * @dataProvider provideTestIsValidMoveOperation + * @covers Title::isValidMoveOperation + * @covers Title::validateFileMoveOperation + */ + public function testIsValidMoveOperation( $source, $target, $expected ) { + $this->setMwGlobals( 'wgContentHandlerUseDB', false ); + $title = Title::newFromText( $source ); + $nt = Title::newFromText( $target ); + $errors = $title->isValidMoveOperation( $nt, false ); + if ( $expected === true ) { + $this->assertTrue( $errors ); + } else { + $errors = $this->flattenErrorsArray( $errors ); + foreach ( (array)$expected as $error ) { + $this->assertContains( $error, $errors ); + } + } + } + + public static function provideTestIsValidMoveOperation() { + return [ + // for Title::isValidMoveOperation + [ 'Some page', '', 'badtitletext' ], + [ 'Test', 'Test', 'selfmove' ], + [ 'Special:FooBar', 'Test', 'immobile-source-namespace' ], + [ 'Test', 'Special:FooBar', 'immobile-target-namespace' ], + [ 'MediaWiki:Common.js', 'Help:Some wikitext page', 'bad-target-model' ], + [ 'Page', 'File:Test.jpg', 'nonfile-cannot-move-to-file' ], + // for Title::validateFileMoveOperation + [ 'File:Test.jpg', 'Page', 'imagenocrossnamespace' ], + ]; + } + + /** + * Auth-less test of Title::userCan + * + * @param array $whitelistRegexp + * @param string $source + * @param string $action + * @param array|string|bool $expected Required error + * + * @covers Title::checkReadPermissions + * @dataProvider dataWgWhitelistReadRegexp + */ + public function testWgWhitelistReadRegexp( $whitelistRegexp, $source, $action, $expected ) { + // $wgWhitelistReadRegexp must be an array. Since the provided test cases + // usually have only one regex, it is more concise to write the lonely regex + // as a string. Thus we cast to an array() to honor $wgWhitelistReadRegexp + // type requisite. + if ( is_string( $whitelistRegexp ) ) { + $whitelistRegexp = [ $whitelistRegexp ]; + } + + $this->setMwGlobals( [ + // So User::isEveryoneAllowed( 'read' ) === false + 'wgGroupPermissions' => [ '*' => [ 'read' => false ] ], + 'wgWhitelistRead' => [ 'some random non sense title' ], + 'wgWhitelistReadRegexp' => $whitelistRegexp, + ] ); + + $title = Title::newFromDBkey( $source ); + + // New anonymous user with no rights + $user = new User; + $user->mRights = []; + $errors = $title->userCan( $action, $user ); + + if ( is_bool( $expected ) ) { + # Forge the assertion message depending on the assertion expectation + $allowableness = $expected + ? " should be allowed" + : " should NOT be allowed"; + $this->assertEquals( + $expected, + $errors, + "User action '$action' on [[$source]] $allowableness." + ); + } else { + $errors = $this->flattenErrorsArray( $errors ); + foreach ( (array)$expected as $error ) { + $this->assertContains( $error, $errors ); + } + } + } + + /** + * Provides test parameter values for testWgWhitelistReadRegexp() + */ + public function dataWgWhitelistReadRegexp() { + $ALLOWED = true; + $DISALLOWED = false; + + return [ + // Everything, if this doesn't work, we're really in trouble + [ '/.*/', 'Main_Page', 'read', $ALLOWED ], + [ '/.*/', 'Main_Page', 'edit', $DISALLOWED ], + + // We validate against the title name, not the db key + [ '/^Main_Page$/', 'Main_Page', 'read', $DISALLOWED ], + // Main page + [ '/^Main/', 'Main_Page', 'read', $ALLOWED ], + [ '/^Main.*/', 'Main_Page', 'read', $ALLOWED ], + // With spaces + [ '/Mic\sCheck/', 'Mic Check', 'read', $ALLOWED ], + // Unicode multibyte + // ...without unicode modifier + [ '/Unicode Test . Yes/', 'Unicode Test Ñ Yes', 'read', $DISALLOWED ], + // ...with unicode modifier + [ '/Unicode Test . Yes/u', 'Unicode Test Ñ Yes', 'read', $ALLOWED ], + // Case insensitive + [ '/MiC ChEcK/', 'mic check', 'read', $DISALLOWED ], + [ '/MiC ChEcK/i', 'mic check', 'read', $ALLOWED ], + + // From DefaultSettings.php: + [ "@^UsEr.*@i", 'User is banned', 'read', $ALLOWED ], + [ "@^UsEr.*@i", 'User:John Doe', 'read', $ALLOWED ], + + // With namespaces: + [ '/^Special:NewPages$/', 'Special:NewPages', 'read', $ALLOWED ], + [ null, 'Special:Newpages', 'read', $DISALLOWED ], + + ]; + } + + public function flattenErrorsArray( $errors ) { + $result = []; + foreach ( $errors as $error ) { + $result[] = $error[0]; + } + + return $result; + } + + /** + * @dataProvider provideGetPageViewLanguage + * @covers Title::getPageViewLanguage + */ + public function testGetPageViewLanguage( $expected, $titleText, $contLang, + $lang, $variant, $msg = '' + ) { + // Setup environnement for this test + $this->setMwGlobals( [ + 'wgDefaultLanguageVariant' => $variant, + 'wgAllowUserJs' => true, + ] ); + $this->setUserLang( $lang ); + $this->setContentLang( $contLang ); + + $title = Title::newFromText( $titleText ); + $this->assertInstanceOf( Title::class, $title, + "Test must be passed a valid title text, you gave '$titleText'" + ); + $this->assertEquals( $expected, + $title->getPageViewLanguage()->getCode(), + $msg + ); + } + + public static function provideGetPageViewLanguage() { + # Format: + # - expected + # - Title name + # - wgContLang (expected in most case) + # - wgLang (on some specific pages) + # - wgDefaultLanguageVariant + # - Optional message + return [ + [ 'fr', 'Help:I_need_somebody', 'fr', 'fr', false ], + [ 'es', 'Help:I_need_somebody', 'es', 'zh-tw', false ], + [ 'zh', 'Help:I_need_somebody', 'zh', 'zh-tw', false ], + + [ 'es', 'Help:I_need_somebody', 'es', 'zh-tw', 'zh-cn' ], + [ 'es', 'MediaWiki:About', 'es', 'zh-tw', 'zh-cn' ], + [ 'es', 'MediaWiki:About/', 'es', 'zh-tw', 'zh-cn' ], + [ 'de', 'MediaWiki:About/de', 'es', 'zh-tw', 'zh-cn' ], + [ 'en', 'MediaWiki:Common.js', 'es', 'zh-tw', 'zh-cn' ], + [ 'en', 'MediaWiki:Common.css', 'es', 'zh-tw', 'zh-cn' ], + [ 'en', 'User:JohnDoe/Common.js', 'es', 'zh-tw', 'zh-cn' ], + [ 'en', 'User:JohnDoe/Monobook.css', 'es', 'zh-tw', 'zh-cn' ], + + [ 'zh-cn', 'Help:I_need_somebody', 'zh', 'zh-tw', 'zh-cn' ], + [ 'zh', 'MediaWiki:About', 'zh', 'zh-tw', 'zh-cn' ], + [ 'zh', 'MediaWiki:About/', 'zh', 'zh-tw', 'zh-cn' ], + [ 'de', 'MediaWiki:About/de', 'zh', 'zh-tw', 'zh-cn' ], + [ 'zh-cn', 'MediaWiki:About/zh-cn', 'zh', 'zh-tw', 'zh-cn' ], + [ 'zh-tw', 'MediaWiki:About/zh-tw', 'zh', 'zh-tw', 'zh-cn' ], + [ 'en', 'MediaWiki:Common.js', 'zh', 'zh-tw', 'zh-cn' ], + [ 'en', 'MediaWiki:Common.css', 'zh', 'zh-tw', 'zh-cn' ], + [ 'en', 'User:JohnDoe/Common.js', 'zh', 'zh-tw', 'zh-cn' ], + [ 'en', 'User:JohnDoe/Monobook.css', 'zh', 'zh-tw', 'zh-cn' ], + + [ 'zh-tw', 'Special:NewPages', 'es', 'zh-tw', 'zh-cn' ], + [ 'zh-tw', 'Special:NewPages', 'zh', 'zh-tw', 'zh-cn' ], + + ]; + } + + /** + * @dataProvider provideBaseTitleCases + * @covers Title::getBaseText + */ + public function testGetBaseText( $title, $expected, $msg = '' ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expected, + $title->getBaseText(), + $msg + ); + } + + public static function provideBaseTitleCases() { + return [ + # Title, expected base, optional message + [ 'User:John_Doe/subOne/subTwo', 'John Doe/subOne' ], + [ 'User:Foo/Bar/Baz', 'Foo/Bar' ], + ]; + } + + /** + * @dataProvider provideRootTitleCases + * @covers Title::getRootText + */ + public function testGetRootText( $title, $expected, $msg = '' ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expected, + $title->getRootText(), + $msg + ); + } + + public static function provideRootTitleCases() { + return [ + # Title, expected base, optional message + [ 'User:John_Doe/subOne/subTwo', 'John Doe' ], + [ 'User:Foo/Bar/Baz', 'Foo' ], + ]; + } + + /** + * @todo Handle $wgNamespacesWithSubpages cases + * @dataProvider provideSubpageTitleCases + * @covers Title::getSubpageText + */ + public function testGetSubpageText( $title, $expected, $msg = '' ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expected, + $title->getSubpageText(), + $msg + ); + } + + public static function provideSubpageTitleCases() { + return [ + # Title, expected base, optional message + [ 'User:John_Doe/subOne/subTwo', 'subTwo' ], + [ 'User:John_Doe/subOne', 'subOne' ], + ]; + } + + public static function provideNewFromTitleValue() { + return [ + [ new TitleValue( NS_MAIN, 'Foo' ) ], + [ new TitleValue( NS_MAIN, 'Foo', 'bar' ) ], + [ new TitleValue( NS_USER, 'Hansi_Maier' ) ], + ]; + } + + /** + * @covers Title::newFromTitleValue + * @dataProvider provideNewFromTitleValue + */ + public function testNewFromTitleValue( TitleValue $value ) { + $title = Title::newFromTitleValue( $value ); + + $dbkey = str_replace( ' ', '_', $value->getText() ); + $this->assertEquals( $dbkey, $title->getDBkey() ); + $this->assertEquals( $value->getNamespace(), $title->getNamespace() ); + $this->assertEquals( $value->getFragment(), $title->getFragment() ); + } + + public static function provideGetTitleValue() { + return [ + [ 'Foo' ], + [ 'Foo#bar' ], + [ 'User:Hansi_Maier' ], + ]; + } + + /** + * @covers Title::getTitleValue + * @dataProvider provideGetTitleValue + */ + public function testGetTitleValue( $text ) { + $title = Title::newFromText( $text ); + $value = $title->getTitleValue(); + + $dbkey = str_replace( ' ', '_', $value->getText() ); + $this->assertEquals( $title->getDBkey(), $dbkey ); + $this->assertEquals( $title->getNamespace(), $value->getNamespace() ); + $this->assertEquals( $title->getFragment(), $value->getFragment() ); + } + + public static function provideGetFragment() { + return [ + [ 'Foo', '' ], + [ 'Foo#bar', 'bar' ], + [ 'Foo#bär', 'bär' ], + + // Inner whitespace is normalized + [ 'Foo#bar_bar', 'bar bar' ], + [ 'Foo#bar bar', 'bar bar' ], + [ 'Foo#bar bar', 'bar bar' ], + + // Leading whitespace is kept, trailing whitespace is trimmed. + // XXX: Is this really want we want? + [ 'Foo#_bar_bar_', ' bar bar' ], + [ 'Foo# bar bar ', ' bar bar' ], + ]; + } + + /** + * @covers Title::getFragment + * @dataProvider provideGetFragment + * + * @param string $full + * @param string $fragment + */ + public function testGetFragment( $full, $fragment ) { + $title = Title::newFromText( $full ); + $this->assertEquals( $fragment, $title->getFragment() ); + } + + /** + * @covers Title::isAlwaysKnown + * @dataProvider provideIsAlwaysKnown + * @param string $page + * @param bool $isKnown + */ + public function testIsAlwaysKnown( $page, $isKnown ) { + $title = Title::newFromText( $page ); + $this->assertEquals( $isKnown, $title->isAlwaysKnown() ); + } + + public static function provideIsAlwaysKnown() { + return [ + [ 'Some nonexistent page', false ], + [ 'UTPage', false ], + [ '#test', true ], + [ 'Special:BlankPage', true ], + [ 'Special:SomeNonexistentSpecialPage', false ], + [ 'MediaWiki:Parentheses', true ], + [ 'MediaWiki:Some nonexistent message', false ], + ]; + } + + /** + * @covers Title::isValid + * @dataProvider provideIsValid + * @param Title $title + * @param bool $isValid + */ + public function testIsValid( Title $title, $isValid ) { + $this->assertEquals( $isValid, $title->isValid(), $title->getPrefixedText() ); + } + + public static function provideIsValid() { + return [ + [ Title::makeTitle( NS_MAIN, '' ), false ], + [ Title::makeTitle( NS_MAIN, '<>' ), false ], + [ Title::makeTitle( NS_MAIN, '|' ), false ], + [ Title::makeTitle( NS_MAIN, '#' ), false ], + [ Title::makeTitle( NS_MAIN, 'Test' ), true ], + [ Title::makeTitle( -33, 'Test' ), false ], + [ Title::makeTitle( 77663399, 'Test' ), false ], + ]; + } + + /** + * @covers Title::isAlwaysKnown + */ + public function testIsAlwaysKnownOnInterwiki() { + $title = Title::makeTitle( NS_MAIN, 'Interwiki link', '', 'externalwiki' ); + $this->assertTrue( $title->isAlwaysKnown() ); + } + + /** + * @covers Title::exists + */ + public function testExists() { + $title = Title::makeTitle( NS_PROJECT, 'New page' ); + $linkCache = LinkCache::singleton(); + + $article = new Article( $title ); + $page = $article->getPage(); + $page->doEditContent( new WikitextContent( 'Some [[link]]' ), 'summary' ); + + // Tell Title it doesn't know whether it exists + $title->mArticleID = -1; + + // Tell the link cache it doesn't exists when it really does + $linkCache->clearLink( $title ); + $linkCache->addBadLinkObj( $title ); + + $this->assertEquals( + false, + $title->exists(), + 'exists() should rely on link cache unless GAID_FOR_UPDATE is used' + ); + $this->assertEquals( + true, + $title->exists( Title::GAID_FOR_UPDATE ), + 'exists() should re-query database when GAID_FOR_UPDATE is used' + ); + } + + public function provideCanHaveTalkPage() { + return [ + 'User page has talk page' => [ + Title::makeTitle( NS_USER, 'Jane' ), true + ], + 'Talke page has talk page' => [ + Title::makeTitle( NS_TALK, 'Foo' ), true + ], + 'Special page cannot have talk page' => [ + Title::makeTitle( NS_SPECIAL, 'Thing' ), false + ], + 'Virtual namespace cannot have talk page' => [ + Title::makeTitle( NS_MEDIA, 'Kitten.jpg' ), false + ], + ]; + } + + /** + * @dataProvider provideCanHaveTalkPage + * @covers Title::canHaveTalkPage + * + * @param Title $title + * @param bool $expected + */ + public function testCanHaveTalkPage( Title $title, $expected ) { + $actual = $title->canHaveTalkPage(); + $this->assertSame( $expected, $actual, $title->getPrefixedDBkey() ); + } + + /** + * @dataProvider provideCanHaveTalkPage + * @covers Title::canTalk + * + * @param Title $title + * @param bool $expected + */ + public function testCanTalk( Title $title, $expected ) { + $actual = $title->canTalk(); + $this->assertSame( $expected, $actual, $title->getPrefixedDBkey() ); + } + + public static function provideGetTalkPage_good() { + return [ + [ Title::makeTitle( NS_MAIN, 'Test' ), Title::makeTitle( NS_TALK, 'Test' ) ], + [ Title::makeTitle( NS_TALK, 'Test' ), Title::makeTitle( NS_TALK, 'Test' ) ], + ]; + } + + /** + * @dataProvider provideGetTalkPage_good + * @covers Title::getTalkPage + */ + public function testGetTalkPage_good( Title $title, Title $expected ) { + $talk = $title->getTalkPage(); + $this->assertSame( + $expected->getPrefixedDBKey(), + $talk->getPrefixedDBKey(), + $title->getPrefixedDBKey() + ); + } + + /** + * @dataProvider provideGetTalkPage_good + * @covers Title::getTalkPageIfDefined + */ + public function testGetTalkPageIfDefined_good( Title $title ) { + $talk = $title->getTalkPageIfDefined(); + $this->assertInstanceOf( + Title::class, + $talk, + $title->getPrefixedDBKey() + ); + } + + public static function provideGetTalkPage_bad() { + return [ + [ Title::makeTitle( NS_SPECIAL, 'Test' ) ], + [ Title::makeTitle( NS_MEDIA, 'Test' ) ], + ]; + } + + /** + * @dataProvider provideGetTalkPage_bad + * @covers Title::getTalkPageIfDefined + */ + public function testGetTalkPageIfDefined_bad( Title $title ) { + $talk = $title->getTalkPageIfDefined(); + $this->assertNull( + $talk, + $title->getPrefixedDBKey() + ); + } + + public function provideCreateFragmentTitle() { + return [ + [ Title::makeTitle( NS_MAIN, 'Test' ), 'foo' ], + [ Title::makeTitle( NS_TALK, 'Test', 'foo' ), '' ], + [ Title::makeTitle( NS_CATEGORY, 'Test', 'foo' ), 'bar' ], + [ Title::makeTitle( NS_MAIN, 'Test1', '', 'interwiki' ), 'baz' ] + ]; + } + + /** + * @covers Title::createFragmentTarget + * @dataProvider provideCreateFragmentTitle + */ + public function testCreateFragmentTitle( Title $title, $fragment ) { + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'InterwikiLoadPrefix' => [ + function ( $prefix, &$iwdata ) { + if ( $prefix === 'interwiki' ) { + $iwdata = [ + 'iw_url' => 'http://example.com/', + 'iw_local' => 0, + 'iw_trans' => 0, + ]; + return false; + } + }, + ], + ] ); + + $fragmentTitle = $title->createFragmentTarget( $fragment ); + + $this->assertEquals( $title->getNamespace(), $fragmentTitle->getNamespace() ); + $this->assertEquals( $title->getText(), $fragmentTitle->getText() ); + $this->assertEquals( $title->getInterwiki(), $fragmentTitle->getInterwiki() ); + $this->assertEquals( $fragment, $fragmentTitle->getFragment() ); + } + + public function provideGetPrefixedText() { + return [ + // ns = 0 + [ + Title::makeTitle( NS_MAIN, 'Foo bar' ), + 'Foo bar' + ], + // ns = 2 + [ + Title::makeTitle( NS_USER, 'Foo bar' ), + 'User:Foo bar' + ], + // ns = 3 + [ + Title::makeTitle( NS_USER_TALK, 'Foo bar' ), + 'User talk:Foo bar' + ], + // fragment not included + [ + Title::makeTitle( NS_MAIN, 'Foo bar', 'fragment' ), + 'Foo bar' + ], + // ns = -2 + [ + Title::makeTitle( NS_MEDIA, 'Foo bar' ), + 'Media:Foo bar' + ], + // non-existent namespace + [ + Title::makeTitle( 100777, 'Foo bar' ), + 'Special:Badtitle/NS100777:Foo bar' + ], + ]; + } + + /** + * @covers Title::getPrefixedText + * @dataProvider provideGetPrefixedText + */ + public function testGetPrefixedText( Title $title, $expected ) { + $this->assertEquals( $expected, $title->getPrefixedText() ); + } + + public function provideGetPrefixedDBKey() { + return [ + // ns = 0 + [ + Title::makeTitle( NS_MAIN, 'Foo_bar' ), + 'Foo_bar' + ], + // ns = 2 + [ + Title::makeTitle( NS_USER, 'Foo_bar' ), + 'User:Foo_bar' + ], + // ns = 3 + [ + Title::makeTitle( NS_USER_TALK, 'Foo_bar' ), + 'User_talk:Foo_bar' + ], + // fragment not included + [ + Title::makeTitle( NS_MAIN, 'Foo_bar', 'fragment' ), + 'Foo_bar' + ], + // ns = -2 + [ + Title::makeTitle( NS_MEDIA, 'Foo_bar' ), + 'Media:Foo_bar' + ], + // non-existent namespace + [ + Title::makeTitle( 100777, 'Foo_bar' ), + 'Special:Badtitle/NS100777:Foo_bar' + ], + ]; + } + + /** + * @covers Title::getPrefixedDBKey + * @dataProvider provideGetPrefixedDBKey + */ + public function testGetPrefixedDBKey( Title $title, $expected ) { + $this->assertEquals( $expected, $title->getPrefixedDBkey() ); + } + + /** + * @covers Title::getFragmentForURL + * @dataProvider provideGetFragmentForURL + * + * @param string $titleStr + * @param string $expected + */ + public function testGetFragmentForURL( $titleStr, $expected ) { + $this->setMwGlobals( [ + 'wgFragmentMode' => [ 'html5' ], + 'wgExternalInterwikiFragmentMode' => 'legacy', + ] ); + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( 'interwiki', + [ + [ + 'iw_prefix' => 'de', + 'iw_url' => 'http://de.wikipedia.org/wiki/', + 'iw_api' => 'http://de.wikipedia.org/w/api.php', + 'iw_wikiid' => 'dewiki', + 'iw_local' => 1, + 'iw_trans' => 0, + ], + [ + 'iw_prefix' => 'zz', + 'iw_url' => 'http://zzwiki.org/wiki/', + 'iw_api' => 'http://zzwiki.org/w/api.php', + 'iw_wikiid' => 'zzwiki', + 'iw_local' => 0, + 'iw_trans' => 0, + ], + ], + __METHOD__, + [ 'IGNORE' ] + ); + + $title = Title::newFromText( $titleStr ); + self::assertEquals( $expected, $title->getFragmentForURL() ); + + $dbw->delete( 'interwiki', '*', __METHOD__ ); + } + + public function provideGetFragmentForURL() { + return [ + [ 'Foo', '' ], + [ 'Foo#ümlåût', '#ümlåût' ], + [ 'de:Foo#Bå®', '#Bå®' ], + [ 'zz:Foo#тест', '#.D1.82.D0.B5.D1.81.D1.82' ], + ]; + } +} |