3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
19 * @author Daniel Kinzler
22 use MediaWiki\Interwiki\InterwikiLookup
;
25 * @covers MediaWikiTitleCodec
29 * ^--- needed because of global state in
31 class MediaWikiTitleCodecTest
extends MediaWikiTestCase
{
33 public function setUp() {
36 $this->setMwGlobals( [
37 'wgAllowUserJs' => false,
38 'wgDefaultLanguageVariant' => false,
39 'wgMetaNamespace' => 'Project',
40 'wgLocalInterwikis' => [ 'localtestiw' ],
41 'wgCapitalLinks' => true,
43 $this->setUserLang( 'en' );
44 $this->setContentLang( 'en' );
48 * Returns a mock GenderCache that will consider a user "female" if the
49 * first part of the user name ends with "a".
53 private function getGenderCache() {
54 $genderCache = $this->getMockBuilder( GenderCache
::class )
55 ->disableOriginalConstructor()
58 $genderCache->expects( $this->any() )
59 ->method( 'getGenderOf' )
60 ->will( $this->returnCallback( function ( $userName ) {
61 return preg_match( '/^[^- _]+a( |_|$)/u', $userName ) ?
'female' : 'male';
68 * Returns a mock InterwikiLookup that only has an isValidInterwiki() method, which recognizes
69 * 'localtestiw' and 'remotetestiw'. All other methods throw.
71 * @return InterwikiLookup
73 private function getInterwikiLookup() : InterwikiLookup
{
74 $iwLookup = $this->createMock( InterwikiLookup
::class );
76 $iwLookup->expects( $this->any() )
77 ->method( 'isValidInterwiki' )
78 ->will( $this->returnCallback( function ( $prefix ) {
79 return $prefix === 'localtestiw' ||
$prefix === 'remotetestiw';
82 $iwLookup->expects( $this->never() )
83 ->method( $this->callback( function ( $name ) {
84 return $name !== 'isValidInterwiki';
91 * Returns a mock NamespaceInfo that has only a hasGenderDistinction() method, which assumes
92 * only NS_USER and NS_USER_TALK have a gender distinction. All other methods throw.
94 * @return NamespaceInfo
96 private function getNamespaceInfo() : NamespaceInfo
{
97 $nsInfo = $this->createMock( NamespaceInfo
::class );
99 $nsInfo->expects( $this->any() )
100 ->method( 'hasGenderDistinction' )
101 ->will( $this->returnCallback( function ( $ns ) {
102 return $ns === NS_USER ||
$ns === NS_USER_TALK
;
105 $nsInfo->expects( $this->never() )
106 ->method( $this->callback( function ( $name ) {
107 return $name !== 'hasGenderDistinction';
113 protected function makeCodec( $lang ) {
114 return new MediaWikiTitleCodec(
115 Language
::factory( $lang ),
116 $this->getGenderCache(),
118 $this->getInterwikiLookup(),
119 $this->getNamespaceInfo()
123 public static function provideFormat() {
125 [ NS_MAIN
, 'Foo_Bar', '', '', 'en', 'Foo Bar' ],
126 [ NS_USER
, 'Hansi_Maier', 'stuff_and_so_on', '', 'en', 'User:Hansi Maier#stuff and so on' ],
127 [ false, 'Hansi_Maier', '', '', 'en', 'Hansi Maier' ],
134 'User talk:hansi maier',
135 'User talk:Hansi maier'
138 // getGenderCache() provides a mock that considers first
139 // names ending in "a" to be female.
140 [ NS_USER
, 'Lisa_Müller', '', '', 'de', 'Benutzerin:Lisa Müller' ],
141 [ NS_MAIN
, 'FooBar', '', 'remotetestiw', 'en', 'remotetestiw:FooBar' ],
146 * @dataProvider provideFormat
148 public function testFormat( $namespace, $text, $fragment, $interwiki, $lang, $expected,
151 if ( $normalized === null ) {
152 $normalized = $expected;
155 $codec = $this->makeCodec( $lang );
156 $actual = $codec->formatTitle( $namespace, $text, $fragment, $interwiki );
158 $this->assertEquals( $expected, $actual, 'formatted' );
161 $parsed = $codec->parseTitle( $actual, NS_MAIN
);
162 $actual2 = $codec->formatTitle(
163 $parsed->getNamespace(),
165 $parsed->getFragment(),
166 $parsed->getInterwiki()
169 $this->assertEquals( $normalized, $actual2, 'normalized after round trip' );
172 public static function provideGetText() {
174 [ NS_MAIN
, 'Foo_Bar', '', 'en', 'Foo Bar' ],
175 [ NS_USER
, 'Hansi_Maier', 'stuff_and_so_on', 'en', 'Hansi Maier' ],
180 * @dataProvider provideGetText
182 public function testGetText( $namespace, $dbkey, $fragment, $lang, $expected ) {
183 $codec = $this->makeCodec( $lang );
184 $title = new TitleValue( $namespace, $dbkey, $fragment );
186 $actual = $codec->getText( $title );
188 $this->assertEquals( $expected, $actual );
191 public static function provideGetPrefixedText() {
193 [ NS_MAIN
, 'Foo_Bar', '', 'en', 'Foo Bar' ],
194 [ NS_USER
, 'Hansi_Maier', 'stuff_and_so_on', 'en', 'User:Hansi Maier' ],
196 // No capitalization or normalization is applied while formatting!
197 [ NS_USER_TALK
, 'hansi__maier', '', 'en', 'User talk:hansi maier' ],
199 // getGenderCache() provides a mock that considers first
200 // names ending in "a" to be female.
201 [ NS_USER
, 'Lisa_Müller', '', 'de', 'Benutzerin:Lisa Müller' ],
202 [ 1000000, 'Invalid_namespace', '', 'en', 'Special:Badtitle/NS1000000:Invalid namespace' ],
207 * @dataProvider provideGetPrefixedText
209 public function testGetPrefixedText( $namespace, $dbkey, $fragment, $lang, $expected ) {
210 $codec = $this->makeCodec( $lang );
211 $title = new TitleValue( $namespace, $dbkey, $fragment );
213 $actual = $codec->getPrefixedText( $title );
215 $this->assertEquals( $expected, $actual );
218 public static function provideGetPrefixedDBkey() {
220 [ NS_MAIN
, 'Foo_Bar', '', '', 'en', 'Foo_Bar' ],
221 [ NS_USER
, 'Hansi_Maier', 'stuff_and_so_on', '', 'en', 'User:Hansi_Maier' ],
223 // No capitalization or normalization is applied while formatting!
224 [ NS_USER_TALK
, 'hansi__maier', '', '', 'en', 'User_talk:hansi__maier' ],
226 // getGenderCache() provides a mock that considers first
227 // names ending in "a" to be female.
228 [ NS_USER
, 'Lisa_Müller', '', '', 'de', 'Benutzerin:Lisa_Müller' ],
230 [ NS_MAIN
, 'Remote_page', '', 'remotetestiw', 'en', 'remotetestiw:Remote_page' ],
232 // non-existent namespace
233 [ 10000000, 'Foobar', '', '', 'en', 'Special:Badtitle/NS10000000:Foobar' ],
238 * @dataProvider provideGetPrefixedDBkey
240 public function testGetPrefixedDBkey( $namespace, $dbkey, $fragment,
241 $interwiki, $lang, $expected
243 $codec = $this->makeCodec( $lang );
244 $title = new TitleValue( $namespace, $dbkey, $fragment, $interwiki );
246 $actual = $codec->getPrefixedDBkey( $title );
248 $this->assertEquals( $expected, $actual );
251 public static function provideGetFullText() {
253 [ NS_MAIN
, 'Foo_Bar', '', 'en', 'Foo Bar' ],
254 [ NS_USER
, 'Hansi_Maier', 'stuff_and_so_on', 'en', 'User:Hansi Maier#stuff and so on' ],
256 // No capitalization or normalization is applied while formatting!
257 [ NS_USER_TALK
, 'hansi__maier', '', 'en', 'User talk:hansi maier' ],
262 * @dataProvider provideGetFullText
264 public function testGetFullText( $namespace, $dbkey, $fragment, $lang, $expected ) {
265 $codec = $this->makeCodec( $lang );
266 $title = new TitleValue( $namespace, $dbkey, $fragment );
268 $actual = $codec->getFullText( $title );
270 $this->assertEquals( $expected, $actual );
273 public static function provideParseTitle() {
274 // TODO: test capitalization and trimming
275 // TODO: test unicode normalization
278 [ ' : Hansi_Maier _ ', NS_MAIN
, 'en',
279 new TitleValue( NS_MAIN
, 'Hansi_Maier', '' ) ],
280 [ 'User:::1', NS_MAIN
, 'de',
281 new TitleValue( NS_USER
, '0:0:0:0:0:0:0:1', '' ) ],
282 [ ' lisa Müller', NS_USER
, 'de',
283 new TitleValue( NS_USER
, 'Lisa_Müller', '' ) ],
284 [ 'benutzerin:lisa Müller#stuff', NS_MAIN
, 'de',
285 new TitleValue( NS_USER
, 'Lisa_Müller', 'stuff' ) ],
287 [ ':Category:Quux', NS_MAIN
, 'en',
288 new TitleValue( NS_CATEGORY
, 'Quux', '' ) ],
289 [ 'Category:Quux', NS_MAIN
, 'en',
290 new TitleValue( NS_CATEGORY
, 'Quux', '' ) ],
291 [ 'Category:Quux', NS_CATEGORY
, 'en',
292 new TitleValue( NS_CATEGORY
, 'Quux', '' ) ],
293 [ 'Quux', NS_CATEGORY
, 'en',
294 new TitleValue( NS_CATEGORY
, 'Quux', '' ) ],
295 [ ':Quux', NS_CATEGORY
, 'en',
296 new TitleValue( NS_MAIN
, 'Quux', '' ) ],
298 // getGenderCache() provides a mock that considers first
299 // names ending in "a" to be female.
301 [ 'a b c', NS_MAIN
, 'en',
302 new TitleValue( NS_MAIN
, 'A_b_c' ) ],
303 [ ' a b c ', NS_MAIN
, 'en',
304 new TitleValue( NS_MAIN
, 'A_b_c' ) ],
305 [ ' _ Foo __ Bar_ _', NS_MAIN
, 'en',
306 new TitleValue( NS_MAIN
, 'Foo_Bar' ) ],
308 // NOTE: cases copied from TitleTest::testSecureAndSplit. Keep in sync.
309 [ 'Sandbox', NS_MAIN
, 'en', ],
310 [ 'A "B"', NS_MAIN
, 'en', ],
311 [ 'A \'B\'', NS_MAIN
, 'en', ],
312 [ '.com', NS_MAIN
, 'en', ],
313 [ '~', NS_MAIN
, 'en', ],
314 [ '"', NS_MAIN
, 'en', ],
315 [ '\'', NS_MAIN
, 'en', ],
317 [ 'Talk:Sandbox', NS_MAIN
, 'en',
318 new TitleValue( NS_TALK
, 'Sandbox' ) ],
319 [ 'Talk:Foo:Sandbox', NS_MAIN
, 'en',
320 new TitleValue( NS_TALK
, 'Foo:Sandbox' ) ],
321 [ 'File:Example.svg', NS_MAIN
, 'en',
322 new TitleValue( NS_FILE
, 'Example.svg' ) ],
323 [ 'File_talk:Example.svg', NS_MAIN
, 'en',
324 new TitleValue( NS_FILE_TALK
, 'Example.svg' ) ],
325 [ 'Foo/.../Sandbox', NS_MAIN
, 'en',
327 [ 'Sandbox/...', NS_MAIN
, 'en',
329 [ 'A~~', NS_MAIN
, 'en',
331 // Length is 256 total, but only title part matters
332 [ 'Category:' . str_repeat( 'x', 248 ), NS_MAIN
, 'en',
333 new TitleValue( NS_CATEGORY
,
334 'X' . str_repeat( 'x', 247 ) ) ],
335 [ str_repeat( 'x', 252 ), NS_MAIN
, 'en',
336 'X' . str_repeat( 'x', 251 ) ],
337 // Test decoding and normalization
338 [ '"ñ"', NS_MAIN
, 'en', new TitleValue( NS_MAIN
, '"ñ"' ) ],
343 * @dataProvider provideParseTitle
345 public function testParseTitle( $text, $ns, $lang, $title = null ) {
346 if ( $title === null ) {
347 $title = str_replace( ' ', '_', trim( $text ) );
350 if ( is_string( $title ) ) {
351 $title = new TitleValue( NS_MAIN
, $title, '' );
354 $codec = $this->makeCodec( $lang );
355 $actual = $codec->parseTitle( $text, $ns );
357 $this->assertEquals( $title, $actual );
360 public static function provideParseTitle_invalid() {
361 // TODO: test unicode errors
370 [ 'Talk:File:Foo.jpg' ],
371 [ 'Talk:localtestiw:Foo' ],
372 [ '::1' ], // only valid in user namespace
373 [ 'User::x' ], // leading ":" in a user name is only valid of IPv6 addresses
375 // NOTE: cases copied from TitleTest::testSecureAndSplit. Keep in sync.
380 // Bad characters forbidden regardless of wgLegalTitleChars
392 // XML/HTML character entity references
393 // Note: Commented out because they are not marked invalid by the PHP test as
394 // Title::newFromText runs Sanitizer::decodeCharReferencesAndNormalize first.
395 // [ 'A é B' ],
397 // [ 'A é B' ],
398 // Subject of NS_TALK does not roundtrip to NS_MAIN
399 [ 'Talk:File:Example.svg' ],
400 // Directory navigation
406 [ 'Foo/../Sandbox' ],
411 [ 'A ~~~~ Signature' ],
412 [ 'A ~~~~~ Timestamp' ],
413 [ str_repeat( 'x', 256 ) ],
414 // Namespace prefix without actual title
422 * @dataProvider provideParseTitle_invalid
424 public function testParseTitle_invalid( $text ) {
425 $this->setExpectedException( MalformedTitleException
::class );
427 $codec = $this->makeCodec( 'en' );
428 $codec->parseTitle( $text, NS_MAIN
);
431 public static function provideGetNamespaceName() {
433 [ NS_MAIN
, 'Foo', 'en', '' ],
434 [ NS_USER
, 'Foo', 'en', 'User' ],
435 [ NS_USER
, 'Hansi Maier', 'de', 'Benutzer' ],
437 // getGenderCache() provides a mock that considers first
438 // names ending in "a" to be female.
439 [ NS_USER
, 'Lisa Müller', 'de', 'Benutzerin' ],
444 * @dataProvider provideGetNamespaceName
446 public function testGetNamespaceName( $namespace, $text, $lang, $expected ) {
447 $codec = $this->makeCodec( $lang );
448 $name = $codec->getNamespaceName( $namespace, $text );
450 $this->assertEquals( $expected, $name );