From: Aryeh Gregor Date: Tue, 13 Aug 2019 11:33:43 +0000 (+0300) Subject: Introduce TitleParser::makeTitleValueSafe() X-Git-Tag: 1.34.0-rc.0~700^2 X-Git-Url: http://git.cyclocoop.org/%28%28?a=commitdiff_plain;h=c503f129c3ed66f5d4865386bc0e26d11fa99a3c;p=lhc%2Fweb%2Fwiklou.git Introduce TitleParser::makeTitleValueSafe() This works exactly the same as Title::makeTitleSafe(), but for TitleValues. 100% test coverage of splitTitleString(). Also added identical parallel tests for Title::makeTitleSafe() to verify that behavior is the same. Along the way, I discovered that TitleValue doesn't allow an empty page name even if there's an interwiki prefix, which is wrong, so I fixed it. Bug: T220966 Change-Id: I4b915244ceee4c1857178dd68dcdf57f1ee32200 --- diff --git a/includes/title/MediaWikiTitleCodec.php b/includes/title/MediaWikiTitleCodec.php index 5021a1c9fe..7e7d85a38e 100644 --- a/includes/title/MediaWikiTitleCodec.php +++ b/includes/title/MediaWikiTitleCodec.php @@ -185,6 +185,40 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser { ); } + /** + * Given a namespace and title, return a TitleValue if valid, or null if invalid. + * + * @param int $namespace + * @param string $text + * @param string $fragment + * @param string $interwiki + * + * @return TitleValue|null + */ + public function makeTitleValueSafe( $namespace, $text, $fragment = '', $interwiki = '' ) { + if ( !$this->nsInfo->exists( $namespace ) ) { + return null; + } + + $canonicalNs = $this->nsInfo->getCanonicalName( $namespace ); + $fullText = $canonicalNs == '' ? $text : "$canonicalNs:$text"; + if ( strval( $interwiki ) != '' ) { + $fullText = "$interwiki:$fullText"; + } + if ( strval( $fragment ) != '' ) { + $fullText .= '#' . $fragment; + } + + try { + $parts = $this->splitTitleString( $fullText ); + } catch ( MalformedTitleException $e ) { + return null; + } + + return new TitleValue( + $parts['namespace'], $parts['dbkey'], $parts['fragment'], $parts['interwiki'] ); + } + /** * @see TitleFormatter::getText() * diff --git a/includes/title/TitleParser.php b/includes/title/TitleParser.php index 8569735e82..0ce5ece015 100644 --- a/includes/title/TitleParser.php +++ b/includes/title/TitleParser.php @@ -43,4 +43,16 @@ interface TitleParser { * @return TitleValue */ public function parseTitle( $text, $defaultNamespace = NS_MAIN ); + + /** + * Given a namespace and title, return a TitleValue if valid, or null if invalid. + * + * @param int $namespace + * @param string $text + * @param string $fragment + * @param string $interwiki + * + * @return TitleValue|null + */ + public function makeTitleValueSafe( $namespace, $text, $fragment = '', $interwiki = '' ); } diff --git a/includes/title/TitleValue.php b/includes/title/TitleValue.php index b657f13ebd..7abe21ba67 100644 --- a/includes/title/TitleValue.php +++ b/includes/title/TitleValue.php @@ -102,8 +102,12 @@ class TitleValue implements LinkTarget { // Sanity check, no full validation or normalization applied here! Assert::parameter( !preg_match( '/^[_ ]|[\r\n\t]|[_ ]$/', $title ), '$title', "invalid name '$title'" ); - Assert::parameter( $title !== '' || ( $fragment !== '' && $namespace === NS_MAIN ), - '$title', 'should not be empty unless namespace is main and fragment is non-empty' ); + Assert::parameter( + $title !== '' || + ( $namespace === NS_MAIN && ( $fragment !== '' || $interwiki !== '' ) ), + '$title', + 'should not be empty unless namespace is main and fragment or interwiki is non-empty' + ); $this->namespace = $namespace; $this->dbkey = strtr( $title, ' ', '_' ); diff --git a/tests/phpunit/includes/title/MediaWikiTitleCodecTest.php b/tests/phpunit/includes/title/MediaWikiTitleCodecTest.php index fdd200b6d6..29a7441beb 100644 --- a/tests/phpunit/includes/title/MediaWikiTitleCodecTest.php +++ b/tests/phpunit/includes/title/MediaWikiTitleCodecTest.php @@ -20,6 +20,7 @@ */ use MediaWiki\Interwiki\InterwikiLookup; +use Wikimedia\TestingAccessWrapper; /** * @covers MediaWikiTitleCodec @@ -88,24 +89,50 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase { } /** - * Returns a mock NamespaceInfo that has only a hasGenderDistinction() method, which assumes - * only NS_USER and NS_USER_TALK have a gender distinction. All other methods throw. + * Returns a mock NamespaceInfo that has only the following methods: + * + * * exists() + * * getCanonicalName() + * * hasGenderDistinction() + * * isCapitalized() + * + * All other methods throw. The only namespaces that exist are NS_SPECIAL, NS_MAIN, NS_TALK, + * NS_USER, and NS_USER_TALK. NS_USER and NS_USER_TALK have gender distinctions. All namespaces + * are capitalized. * * @return NamespaceInfo */ private function getNamespaceInfo() : NamespaceInfo { + $canonicalNamespaces = [ + NS_SPECIAL => 'Special', + NS_MAIN => '', + NS_TALK => 'Talk', + NS_USER => 'User', + NS_USER_TALK => 'User_talk', + ]; + $nsInfo = $this->createMock( NamespaceInfo::class ); - $nsInfo->expects( $this->any() ) - ->method( 'hasGenderDistinction' ) + $nsInfo->method( 'exists' ) + ->will( $this->returnCallback( function ( $ns ) use ( $canonicalNamespaces ) { + return isset( $canonicalNamespaces[$ns] ); + } ) ); + + $nsInfo->method( 'getCanonicalName' ) + ->will( $this->returnCallback( function ( $ns ) use ( $canonicalNamespaces ) { + return $canonicalNamespaces[$ns] ?? false; + } ) ); + + $nsInfo->method( 'hasGenderDistinction' ) ->will( $this->returnCallback( function ( $ns ) { return $ns === NS_USER || $ns === NS_USER_TALK; } ) ); - $nsInfo->expects( $this->never() ) - ->method( $this->callback( function ( $name ) { - return $name !== 'hasGenderDistinction'; - } ) ); + $nsInfo->method( 'isCapitalized' )->willReturn( true ); + + $nsInfo->expects( $this->never() )->method( $this->anythingBut( + 'exists', 'getCanonicalName', 'hasGenderDistinction', 'isCapitalized' + ) ); return $nsInfo; } @@ -114,7 +141,7 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase { return new MediaWikiTitleCodec( Language::factory( $lang ), $this->getGenderCache(), - [], + [ 'localtestiw' ], $this->getInterwikiLookup(), $this->getNamespaceInfo() ); @@ -436,6 +463,370 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase { $codec->parseTitle( $text, NS_MAIN ); } + /** + * @dataProvider provideMakeTitleValueSafe + * @covers IP::sanitizeIP + */ + public function testMakeTitleValueSafe( + $expected, $ns, $text, $fragment = '', $interwiki = '', $lang = 'en' + ) { + $codec = $this->makeCodec( $lang ); + $this->assertEquals( $expected, + $codec->makeTitleValueSafe( $ns, $text, $fragment, $interwiki ) ); + } + + /** + * @dataProvider provideMakeTitleValueSafe + * @covers Title::makeTitleSafe + * @covers Title::makeName + * @covers Title::secureAndSplit + * @covers IP::sanitizeIP + */ + public function testMakeTitleSafe( + $expected, $ns, $text, $fragment = '', $interwiki = '', $lang = 'en' + ) { + $codec = $this->makeCodec( $lang ); + $this->setService( 'TitleParser', $codec ); + $this->setService( 'TitleFormatter', $codec ); + + $actual = Title::makeTitleSafe( $ns, $text, $fragment, $interwiki ); + // We need to reset some members so equality testing works + if ( $actual ) { + $wrapper = TestingAccessWrapper::newFromObject( $actual ); + $wrapper->mArticleID = $actual->getNamespace() === NS_SPECIAL ? 0 : -1; + $wrapper->mLocalInterwiki = false; + $wrapper->mUserCaseDBKey = null; + } + $this->assertEquals( Title::castFromLinkTarget( $expected ), $actual ); + } + + public static function provideMakeTitleValueSafe() { + $ret = [ + 'Nonexistent NS' => [ null, 942929, 'Test' ], + 'Simple page' => [ new TitleValue( NS_MAIN, 'Test' ), NS_MAIN, 'Test' ], + + // Fragments + 'Passed fragment' => [ + new TitleValue( NS_MAIN, 'Test', 'Fragment' ), + NS_MAIN, 'Test', 'Fragment' + ], + 'Embedded fragment' => [ + new TitleValue( NS_MAIN, 'Test', 'Fragment' ), + NS_MAIN, 'Test#Fragment' + ], + 'Passed fragment with spaces' => [ + // XXX Leading space is okay in fragment? + new TitleValue( NS_MAIN, 'Test', ' Frag ment' ), + NS_MAIN, ' Test ', " Frag_ment " + ], + 'Embedded fragment with spaces' => [ + // XXX Leading space is okay in fragment? + new TitleValue( NS_MAIN, 'Test', ' Frag ment' ), + NS_MAIN, " Test # Frag_ment " + ], + // XXX Is it correct that these aren't normalized to spaces? + 'Passed fragment with leading tab' => [ null, NS_MAIN, "\tTest\t", "\tFragment" ], + 'Embedded fragment with leading tab' => [ null, NS_MAIN, "\tTest\t#\tFragment" ], + 'Passed fragment with trailing tab' => [ null, NS_MAIN, "\tTest\t", "Fragment\t" ], + 'Embedded fragment with trailing tab' => [ null, NS_MAIN, "\tTest\t#Fragment\t" ], + 'Passed fragment with interior tab' => [ null, NS_MAIN, "\tTest\t", "Frag\tment" ], + 'Embedded fragment with interior tab' => [ null, NS_MAIN, "\tTest\t#\tFrag\tment" ], + + // Interwikis + 'Passed local interwiki' => [ + new TitleValue( NS_MAIN, 'Test' ), + NS_MAIN, 'Test', '', 'localtestiw' + ], + 'Embedded local interwiki' => [ + new TitleValue( NS_MAIN, 'Test' ), + NS_MAIN, 'localtestiw:Test' + ], + 'Passed remote interwiki' => [ + new TitleValue( NS_MAIN, 'Test', '', 'remotetestiw' ), + NS_MAIN, 'Test', '', 'remotetestiw' + ], + 'Embedded remote interwiki' => [ + new TitleValue( NS_MAIN, 'Test', '', 'remotetestiw' ), + NS_MAIN, 'remotetestiw:Test' + ], + // XXX Are these correct? Interwiki prefixes are case-sensitive? + 'Passed local interwiki with different case' => [ + new TitleValue( NS_MAIN, 'LocalTestIW:Test' ), + NS_MAIN, 'Test', '', 'LocalTestIW' + ], + 'Embedded local interwiki with different case' => [ + new TitleValue( NS_MAIN, 'LocalTestIW:Test' ), + NS_MAIN, 'LocalTestIW:Test' + ], + 'Passed remote interwiki with different case' => [ + new TitleValue( NS_MAIN, 'RemoteTestIW:Test' ), + NS_MAIN, 'Test', '', 'RemoteTestIW' + ], + 'Embedded remote interwiki with different case' => [ + new TitleValue( NS_MAIN, 'RemoteTestIW:Test' ), + NS_MAIN, 'RemoteTestIW:Test' + ], + 'Passed local interwiki with lowercase page name' => [ + new TitleValue( NS_MAIN, 'Test' ), + NS_MAIN, 'test', '', 'localtestiw' + ], + 'Embedded local interwiki with lowercase page name' => [ + new TitleValue( NS_MAIN, 'Test' ), + NS_MAIN, 'localtestiw:test' + ], + // For remote we don't auto-capitalize + 'Passed remote interwiki with lowercase page name' => [ + new TitleValue( NS_MAIN, 'test', '', 'remotetestiw' ), + NS_MAIN, 'test', '', 'remotetestiw' + ], + 'Embedded remote interwiki with lowercase page name' => [ + new TitleValue( NS_MAIN, 'test', '', 'remotetestiw' ), + NS_MAIN, 'remotetestiw:test' + ], + + // Fragment and interwiki + 'Fragment and local interwiki' => [ + new TitleValue( NS_MAIN, 'Test', 'Fragment' ), + NS_MAIN, 'Test', 'Fragment', 'localtestiw' + ], + 'Fragment and remote interwiki' => [ + new TitleValue( NS_MAIN, 'Test', 'Fragment', 'remotetestiw' ), + NS_MAIN, 'Test', 'Fragment', 'remotetestiw' + ], + 'Fragment and local interwiki and non-main namespace' => [ + new TitleValue( NS_TALK, 'Test', 'Fragment' ), + NS_TALK, 'Test', 'Fragment', 'localtestiw' + ], + // We don't know the foreign wiki's namespaces, so it will always be NS_MAIN + 'Fragment and remote interwiki and non-main namespace' => [ + new TitleValue( NS_MAIN, 'Talk:Test', 'Fragment', 'remotetestiw' ), + NS_TALK, 'Test', 'Fragment', 'remotetestiw' + ], + + // Whitespace normalization and Unicode stripping + 'Name with space' => [ + new TitleValue( NS_MAIN, 'Test_test' ), + NS_MAIN, 'Test test' + ], + 'Unicode bidi override characters' => [ + new TitleValue( NS_MAIN, 'Test' ), + NS_MAIN, "\u{200E}T\u{200F}e\u{202A}s\u{202B}t\u{202C}\u{202D}\u{202E}" + ], + 'Invalid UTF-8 sequence' => [ null, NS_MAIN, "Te\x80\xf0st" ], + 'Whitespace collapsing' => [ + new TitleValue( NS_MAIN, 'Test_test' ), + NS_MAIN, "Test _\u{00A0}\u{1680}\u{180E}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}" . + "\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}" . + "\u{205F}\u{3000}test" + ], + 'UTF8_REPLACEMENT' => [ null, NS_MAIN, UtfNormal\Constants::UTF8_REPLACEMENT ], + + // Namespace prefixes + 'Talk:Test' => [ + new TitleValue( NS_TALK, 'Test' ), + NS_MAIN, 'Talk:Test' + ], + 'Test in talk NS' => [ + new TitleValue( NS_TALK, 'Test' ), + NS_TALK, 'Test' + ], + 'Talkk:Test' => [ + new TitleValue( NS_MAIN, 'Talkk:Test' ), + NS_MAIN, 'Talkk:Test' + ], + 'Talk:Talk:Test' => [ null, NS_MAIN, 'Talk:Talk:Test' ], + 'Talk:User:Test' => [ null, NS_MAIN, 'Talk:User:Test' ], + 'User:Talk:Test' => [ + new TitleValue( NS_USER, 'Talk:Test' ), + NS_MAIN, 'User:Talk:Test' + ], + 'User:Test in talk NS' => [ null, NS_TALK, 'User:Test' ], + 'Talk:Test in talk NS' => [ null, NS_TALK, 'Talk:Test' ], + 'User:Test in user NS' => [ + new TitleValue( NS_USER, 'User:Test' ), + NS_USER, 'User:Test' + ], + 'Talk:Test in user NS' => [ + new TitleValue( NS_USER, 'Talk:Test' ), + NS_USER, 'Talk:Test' + ], + + // Initial colon + ':Test' => [ + new TitleValue( NS_MAIN, 'Test' ), + NS_MAIN, ':Test' + ], + ':Talk:Test' => [ + new TitleValue( NS_TALK, 'Test' ), + NS_MAIN, ':Talk:Test' + ], + ':localtestiw:Test' => [ + new TitleValue( NS_MAIN, 'Test' ), + NS_MAIN, ':localtestiw:Test' + ], + ':remotetestiw:Test' => [ + new TitleValue( NS_MAIN, 'Test', '', 'remotetestiw' ), + NS_MAIN, ':remotetestiw:Test' + ], + // XXX Is this correct? Why is it different from remote? + 'localtestiw::Test' => [ null, NS_MAIN, 'localtestiw::Test' ], + 'remotetestiw::Test' => [ + new TitleValue( NS_MAIN, 'Test', '', 'remotetestiw' ), + NS_MAIN, 'remotetestiw::Test' + ], + // XXX Is this correct? Why is it different from remote? + 'localtestiw:: Test' => [ null, NS_MAIN, 'localtestiw:: Test' ], + 'remotetestiw:: Test' => [ + new TitleValue( NS_MAIN, 'Test', '', 'remotetestiw' ), + NS_MAIN, 'remotetestiw:: Test' + ], + + // Empty titles + 'Empty title' => [ null, NS_MAIN, '' ], + 'Empty title with namespace' => [ null, NS_USER, '' ], + 'Local interwiki with empty page name' => [ + new TitleValue( NS_MAIN, 'Main_Page' ), + NS_MAIN, 'localtestiw:' + ], + 'Remote interwiki with empty page name' => [ + // XXX Is this correct? This is supposed to redirect to the main page remotely? + new TitleValue( NS_MAIN, '', '', 'remotetestiw' ), + NS_MAIN, 'remotetestiw:' + ], + + // Whitespace-only titles + 'Whitespace-only title' => [ null, NS_MAIN, "\t\n" ], + 'Whitespace-only title with namespace' => [ null, NS_USER, " _ " ], + 'Local interwiki with whitespace-only page name' => [ + // XXX Is whitespace-only really supposed to be different from empty? + null, + NS_MAIN, "localtestiw:_\t" + ], + 'Remote interwiki with whitespace-only page name' => [ + // XXX Is whitespace-only really supposed to be different from empty? + null, + NS_MAIN, "remotetestiw:\t_\n\r" + ], + + // Namespace and interwiki + 'Talk:localtestiw:Test' => [ null, NS_MAIN, 'Talk:localtestiw:Test' ], + 'Talk:remotetestiw:Test' => [ null, NS_MAIN, 'Talk:remotetestiw:Test' ], + 'User:localtestiw:Test' => [ + new TitleValue( NS_USER, 'Localtestiw:Test' ), + NS_MAIN, 'User:localtestiw:Test' + ], + 'User:remotetestiw:Test' => [ + new TitleValue( NS_USER, 'Remotetestiw:Test' ), + NS_MAIN, 'User:remotetestiw:Test' + ], + 'localtestiw:Test in user namespace' => [ + new TitleValue( NS_USER, 'Localtestiw:Test' ), + NS_USER, 'localtestiw:Test' + ], + 'remotetestiw:Test in user namespace' => [ + new TitleValue( NS_USER, 'Remotetestiw:Test' ), + NS_USER, 'remotetestiw:Test' + ], + 'localtestiw:talk:test' => [ + new TitleValue( NS_TALK, 'Test' ), + NS_MAIN, 'localtestiw:talk:test' + ], + 'remotetestiw:talk:test' => [ + new TitleValue( NS_MAIN, 'talk:test', '', 'remotetestiw' ), + NS_MAIN, 'remotetestiw:talk:test' + ], + + // Invalid chars + 'Test[test' => [ null, NS_MAIN, 'Test[test' ], + + // Long titles + '255 chars long' => [ + new TitleValue( NS_MAIN, str_repeat( 'A', 255 ) ), + NS_MAIN, str_repeat( 'A', 255 ) + ], + '255 chars long in user NS' => [ + new TitleValue( NS_USER, str_repeat( 'A', 255 ) ), + NS_USER, str_repeat( 'A', 255 ) + ], + 'User:255 chars long' => [ + new TitleValue( NS_USER, str_repeat( 'A', 255 ) ), + NS_MAIN, 'User:' . str_repeat( 'A', 255 ) + ], + '256 chars long' => [ null, NS_MAIN, str_repeat( 'A', 256 ) ], + '256 chars long in user NS' => [ null, NS_USER, str_repeat( 'A', 256 ) ], + 'User:256 chars long' => [ null, NS_MAIN, 'User:' . str_repeat( 'A', 256 ) ], + + '512 chars long in special NS' => [ + new TitleValue( NS_SPECIAL, str_repeat( 'A', 512 ) ), + NS_SPECIAL, str_repeat( 'A', 512 ) + ], + 'Special:512 chars long' => [ + new TitleValue( NS_SPECIAL, str_repeat( 'A', 512 ) ), + NS_MAIN, 'Special:' . str_repeat( 'A', 512 ) + ], + '513 chars long in special NS' => [ null, NS_SPECIAL, str_repeat( 'A', 513 ) ], + 'Special:513 chars long' => [ null, NS_MAIN, 'Special:' . str_repeat( 'A', 513 ) ], + + // IP addresses + 'User:000.000.000' => [ + new TitleValue( NS_USER, '000.000.000' ), + NS_MAIN, 'User:000.000.000' + ], + 'User:000.000.000.000' => [ + new TitleValue( NS_USER, '0.0.0.0' ), + NS_MAIN, 'User:000.000.000.000' + ], + '000.000.000.000' => [ + new TitleValue( NS_MAIN, '000.000.000.000' ), + NS_MAIN, '000.000.000.000' + ], + 'User:1.1.256.000' => [ + new TitleValue( NS_USER, '1.1.256.000' ), + NS_MAIN, 'User:1.1.256.000' + ], + 'User:1.1.255.000' => [ + new TitleValue( NS_USER, '1.1.255.0' ), + NS_MAIN, 'User:1.1.255.000' + ], + // TODO More IP address sanitization tests + ]; + + // Invalid and valid dots + foreach ( [ '.', '..', '...' ] as $dots ) { + foreach ( [ '?', '?/', '?/Test', 'Test/?/Test', '/?', 'Test/?', '?Test', 'Test?Test', + 'Test?' ] as $pattern ) { + $test = str_replace( '?', $dots, $pattern ); + if ( $dots === '...' || in_array( $pattern, [ '?Test', 'Test?Test', 'Test?' ] ) ) { + $expectedMain = new TitleValue( NS_MAIN, $test ); + $expectedUser = new TitleValue( NS_USER, $test ); + } else { + $expectedMain = $expectedUser = null; + } + $ret[$test] = [ $expectedMain, NS_MAIN, $test ]; + $ret["$test in user NS"] = [ $expectedUser, NS_USER, $test ]; + $ret["User:$test"] = [ $expectedUser, NS_MAIN, "User:$test" ]; + } + } + + // Invalid and valid tildes + foreach ( [ '~~', '~~~' ] as $tildes ) { + foreach ( [ '?', 'Test?', '?Test', 'Test?Test' ] as $pattern ) { + $test = str_replace( '?', $tildes, $pattern ); + if ( $tildes === '~~' ) { + $expectedMain = new TitleValue( NS_MAIN, $test ); + $expectedUser = new TitleValue( NS_USER, $test ); + } else { + $expectedMain = $expectedUser = null; + } + $ret[$test] = [ $expectedMain, NS_MAIN, $test ]; + $ret["$test in user NS"] = [ $expectedUser, NS_USER, $test ]; + $ret["User:$test"] = [ $expectedUser, NS_MAIN, "User:$test" ]; + } + } + + return $ret; + } + public static function provideGetNamespaceName() { return [ [ NS_MAIN, 'Foo', 'en', '' ], diff --git a/tests/phpunit/unit/includes/title/TitleValueTest.php b/tests/phpunit/unit/includes/title/TitleValueTest.php index b95efdfd52..8256c7c0d5 100644 --- a/tests/phpunit/unit/includes/title/TitleValueTest.php +++ b/tests/phpunit/unit/includes/title/TitleValueTest.php @@ -29,6 +29,8 @@ class TitleValueTest extends \MediaWikiUnitTestCase { public function goodConstructorProvider() { return [ [ NS_MAIN, '', 'fragment', '', true, false ], + [ NS_MAIN, '', '', 'interwiki', false, true ], + [ NS_MAIN, '', 'fragment', 'interwiki', true, true ], [ NS_USER, 'TestThis', 'stuff', '', true, false ], [ NS_USER, 'TestThis', '', 'baz', false, true ], [ NS_MAIN, 'foo bar', '', '', false, false ], @@ -63,6 +65,7 @@ class TitleValueTest extends \MediaWikiUnitTestCase { [ NS_MAIN, 5, 'fragment', '' ], [ NS_MAIN, null, 'fragment', '' ], [ NS_USER, '', 'fragment', '' ], + [ NS_USER, '', '', 'interwiki' ], [ NS_MAIN, 'bar_', '', '' ], [ NS_MAIN, '_foo', '', '' ], [ NS_MAIN, ' eek ', '', '' ],