Merge "Selenium: replace UserLoginPage with BlankPage where possible"
[lhc/web/wiklou.git] / tests / phpunit / includes / title / MediaWikiTitleCodecTest.php
1 <?php
2 /**
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.
7 *
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.
12 *
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
17 *
18 * @file
19 * @author Daniel Kinzler
20 */
21
22 use MediaWiki\Interwiki\InterwikiLookup;
23
24 /**
25 * @covers MediaWikiTitleCodec
26 *
27 * @group Title
28 * @group Database
29 * ^--- needed because of global state in
30 */
31 class MediaWikiTitleCodecTest extends MediaWikiTestCase {
32
33 public function setUp() {
34 parent::setUp();
35
36 $this->setMwGlobals( [
37 'wgAllowUserJs' => false,
38 'wgDefaultLanguageVariant' => false,
39 'wgMetaNamespace' => 'Project',
40 'wgLocalInterwikis' => [ 'localtestiw' ],
41 'wgCapitalLinks' => true,
42 ] );
43 $this->setUserLang( 'en' );
44 $this->setContentLang( 'en' );
45 }
46
47 /**
48 * Returns a mock GenderCache that will consider a user "female" if the
49 * first part of the user name ends with "a".
50 *
51 * @return GenderCache
52 */
53 private function getGenderCache() {
54 $genderCache = $this->getMockBuilder( GenderCache::class )
55 ->disableOriginalConstructor()
56 ->getMock();
57
58 $genderCache->expects( $this->any() )
59 ->method( 'getGenderOf' )
60 ->will( $this->returnCallback( function ( $userName ) {
61 return preg_match( '/^[^- _]+a( |_|$)/u', $userName ) ? 'female' : 'male';
62 } ) );
63
64 return $genderCache;
65 }
66
67 /**
68 * Returns a mock InterwikiLookup that only has an isValidInterwiki() method, which recognizes
69 * 'localtestiw' and 'remotetestiw'. All other methods throw.
70 *
71 * @return InterwikiLookup
72 */
73 private function getInterwikiLookup() : InterwikiLookup {
74 $iwLookup = $this->createMock( InterwikiLookup::class );
75
76 $iwLookup->expects( $this->any() )
77 ->method( 'isValidInterwiki' )
78 ->will( $this->returnCallback( function ( $prefix ) {
79 return $prefix === 'localtestiw' || $prefix === 'remotetestiw';
80 } ) );
81
82 $iwLookup->expects( $this->never() )
83 ->method( $this->callback( function ( $name ) {
84 return $name !== 'isValidInterwiki';
85 } ) );
86
87 return $iwLookup;
88 }
89
90 /**
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.
93 *
94 * @return NamespaceInfo
95 */
96 private function getNamespaceInfo() : NamespaceInfo {
97 $nsInfo = $this->createMock( NamespaceInfo::class );
98
99 $nsInfo->expects( $this->any() )
100 ->method( 'hasGenderDistinction' )
101 ->will( $this->returnCallback( function ( $ns ) {
102 return $ns === NS_USER || $ns === NS_USER_TALK;
103 } ) );
104
105 $nsInfo->expects( $this->never() )
106 ->method( $this->callback( function ( $name ) {
107 return $name !== 'hasGenderDistinction';
108 } ) );
109
110 return $nsInfo;
111 }
112
113 protected function makeCodec( $lang ) {
114 return new MediaWikiTitleCodec(
115 Language::factory( $lang ),
116 $this->getGenderCache(),
117 [],
118 $this->getInterwikiLookup(),
119 $this->getNamespaceInfo()
120 );
121 }
122
123 public static function provideFormat() {
124 return [
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' ],
128 [
129 NS_USER_TALK,
130 'hansi__maier',
131 '',
132 '',
133 'en',
134 'User talk:hansi maier',
135 'User talk:Hansi maier'
136 ],
137
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' ],
142 ];
143 }
144
145 /**
146 * @dataProvider provideFormat
147 */
148 public function testFormat( $namespace, $text, $fragment, $interwiki, $lang, $expected,
149 $normalized = null
150 ) {
151 if ( $normalized === null ) {
152 $normalized = $expected;
153 }
154
155 $codec = $this->makeCodec( $lang );
156 $actual = $codec->formatTitle( $namespace, $text, $fragment, $interwiki );
157
158 $this->assertEquals( $expected, $actual, 'formatted' );
159
160 // test round trip
161 $parsed = $codec->parseTitle( $actual, NS_MAIN );
162 $actual2 = $codec->formatTitle(
163 $parsed->getNamespace(),
164 $parsed->getText(),
165 $parsed->getFragment(),
166 $parsed->getInterwiki()
167 );
168
169 $this->assertEquals( $normalized, $actual2, 'normalized after round trip' );
170 }
171
172 public static function provideGetText() {
173 return [
174 [ NS_MAIN, 'Foo_Bar', '', 'en', 'Foo Bar' ],
175 [ NS_USER, 'Hansi_Maier', 'stuff_and_so_on', 'en', 'Hansi Maier' ],
176 ];
177 }
178
179 /**
180 * @dataProvider provideGetText
181 */
182 public function testGetText( $namespace, $dbkey, $fragment, $lang, $expected ) {
183 $codec = $this->makeCodec( $lang );
184 $title = new TitleValue( $namespace, $dbkey, $fragment );
185
186 $actual = $codec->getText( $title );
187
188 $this->assertEquals( $expected, $actual );
189 }
190
191 public static function provideGetPrefixedText() {
192 return [
193 [ NS_MAIN, 'Foo_Bar', '', 'en', 'Foo Bar' ],
194 [ NS_USER, 'Hansi_Maier', 'stuff_and_so_on', 'en', 'User:Hansi Maier' ],
195
196 // No capitalization or normalization is applied while formatting!
197 [ NS_USER_TALK, 'hansi__maier', '', 'en', 'User talk:hansi maier' ],
198
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' ],
203 ];
204 }
205
206 /**
207 * @dataProvider provideGetPrefixedText
208 */
209 public function testGetPrefixedText( $namespace, $dbkey, $fragment, $lang, $expected ) {
210 $codec = $this->makeCodec( $lang );
211 $title = new TitleValue( $namespace, $dbkey, $fragment );
212
213 $actual = $codec->getPrefixedText( $title );
214
215 $this->assertEquals( $expected, $actual );
216 }
217
218 public static function provideGetPrefixedDBkey() {
219 return [
220 [ NS_MAIN, 'Foo_Bar', '', '', 'en', 'Foo_Bar' ],
221 [ NS_USER, 'Hansi_Maier', 'stuff_and_so_on', '', 'en', 'User:Hansi_Maier' ],
222
223 // No capitalization or normalization is applied while formatting!
224 [ NS_USER_TALK, 'hansi__maier', '', '', 'en', 'User_talk:hansi__maier' ],
225
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' ],
229
230 [ NS_MAIN, 'Remote_page', '', 'remotetestiw', 'en', 'remotetestiw:Remote_page' ],
231
232 // non-existent namespace
233 [ 10000000, 'Foobar', '', '', 'en', 'Special:Badtitle/NS10000000:Foobar' ],
234 ];
235 }
236
237 /**
238 * @dataProvider provideGetPrefixedDBkey
239 */
240 public function testGetPrefixedDBkey( $namespace, $dbkey, $fragment,
241 $interwiki, $lang, $expected
242 ) {
243 $codec = $this->makeCodec( $lang );
244 $title = new TitleValue( $namespace, $dbkey, $fragment, $interwiki );
245
246 $actual = $codec->getPrefixedDBkey( $title );
247
248 $this->assertEquals( $expected, $actual );
249 }
250
251 public static function provideGetFullText() {
252 return [
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' ],
255
256 // No capitalization or normalization is applied while formatting!
257 [ NS_USER_TALK, 'hansi__maier', '', 'en', 'User talk:hansi maier' ],
258 ];
259 }
260
261 /**
262 * @dataProvider provideGetFullText
263 */
264 public function testGetFullText( $namespace, $dbkey, $fragment, $lang, $expected ) {
265 $codec = $this->makeCodec( $lang );
266 $title = new TitleValue( $namespace, $dbkey, $fragment );
267
268 $actual = $codec->getFullText( $title );
269
270 $this->assertEquals( $expected, $actual );
271 }
272
273 public static function provideParseTitle() {
274 // TODO: test capitalization and trimming
275 // TODO: test unicode normalization
276
277 return [
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' ) ],
286
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', '' ) ],
297
298 // getGenderCache() provides a mock that considers first
299 // names ending in "a" to be female.
300
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' ) ],
307
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', ],
316
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',
326 'Foo/.../Sandbox' ],
327 [ 'Sandbox/...', NS_MAIN, 'en',
328 'Sandbox/...' ],
329 [ 'A~~', NS_MAIN, 'en',
330 'A~~' ],
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 [ '&quot;n&#x303;&#34;', NS_MAIN, 'en', new TitleValue( NS_MAIN, '"ñ"' ) ],
339 [ 'X#n&#x303;', NS_MAIN, 'en', new TitleValue( NS_MAIN, 'X', 'ñ' ) ],
340 // target section parsing
341 'empty fragment' => [ 'X#', NS_MAIN, 'en', new TitleValue( NS_MAIN, 'X' ) ],
342 'double hash' => [ 'X##', NS_MAIN, 'en', new TitleValue( NS_MAIN, 'X', '#' ) ],
343 'fragment with hash' => [ 'X#z#z', NS_MAIN, 'en', new TitleValue( NS_MAIN, 'X', 'z#z' ) ],
344 'fragment with space' => [ 'X#z z', NS_MAIN, 'en', new TitleValue( NS_MAIN, 'X', 'z z' ) ],
345 'fragment with percent' => [ 'X#z%z', NS_MAIN, 'en', new TitleValue( NS_MAIN, 'X', 'z%z' ) ],
346 'fragment with amp' => [ 'X#z&z', NS_MAIN, 'en', new TitleValue( NS_MAIN, 'X', 'z&z' ) ],
347 ];
348 }
349
350 /**
351 * @dataProvider provideParseTitle
352 */
353 public function testParseTitle( $text, $ns, $lang, $title = null ) {
354 if ( $title === null ) {
355 $title = str_replace( ' ', '_', trim( $text ) );
356 }
357
358 if ( is_string( $title ) ) {
359 $title = new TitleValue( NS_MAIN, $title, '' );
360 }
361
362 $codec = $this->makeCodec( $lang );
363 $actual = $codec->parseTitle( $text, $ns );
364
365 $this->assertEquals( $title, $actual );
366 }
367
368 public static function provideParseTitle_invalid() {
369 // TODO: test unicode errors
370
371 return [
372 [ '#' ],
373 [ '::' ],
374 [ '::xx' ],
375 [ '::##' ],
376 [ ' :: x' ],
377
378 [ 'Talk:File:Foo.jpg' ],
379 [ 'Talk:localtestiw:Foo' ],
380 [ '::1' ], // only valid in user namespace
381 [ 'User::x' ], // leading ":" in a user name is only valid of IPv6 addresses
382
383 // NOTE: cases copied from TitleTest::testSecureAndSplit. Keep in sync.
384 [ '' ],
385 [ ':' ],
386 [ '__ __' ],
387 [ ' __ ' ],
388 // Bad characters forbidden regardless of wgLegalTitleChars
389 [ 'A [ B' ],
390 [ 'A ] B' ],
391 [ 'A { B' ],
392 [ 'A } B' ],
393 [ 'A < B' ],
394 [ 'A > B' ],
395 [ 'A | B' ],
396 // URL encoding
397 [ 'A%20B' ],
398 [ 'A%23B' ],
399 [ 'A%2523B' ],
400 // XML/HTML character entity references
401 // Note: Commented out because they are not marked invalid by the PHP test as
402 // Title::newFromText runs Sanitizer::decodeCharReferencesAndNormalize first.
403 // [ 'A &eacute; B' ],
404 // [ 'A &#233; B' ],
405 // [ 'A &#x00E9; B' ],
406 // Subject of NS_TALK does not roundtrip to NS_MAIN
407 [ 'Talk:File:Example.svg' ],
408 // Directory navigation
409 [ '.' ],
410 [ '..' ],
411 [ './Sandbox' ],
412 [ '../Sandbox' ],
413 [ 'Foo/./Sandbox' ],
414 [ 'Foo/../Sandbox' ],
415 [ 'Sandbox/.' ],
416 [ 'Sandbox/..' ],
417 // Tilde
418 [ 'A ~~~ Name' ],
419 [ 'A ~~~~ Signature' ],
420 [ 'A ~~~~~ Timestamp' ],
421 [ str_repeat( 'x', 256 ) ],
422 // Namespace prefix without actual title
423 [ 'Talk:' ],
424 [ 'Category: ' ],
425 [ 'Category: #bar' ]
426 ];
427 }
428
429 /**
430 * @dataProvider provideParseTitle_invalid
431 */
432 public function testParseTitle_invalid( $text ) {
433 $this->setExpectedException( MalformedTitleException::class );
434
435 $codec = $this->makeCodec( 'en' );
436 $codec->parseTitle( $text, NS_MAIN );
437 }
438
439 public static function provideGetNamespaceName() {
440 return [
441 [ NS_MAIN, 'Foo', 'en', '' ],
442 [ NS_USER, 'Foo', 'en', 'User' ],
443 [ NS_USER, 'Hansi Maier', 'de', 'Benutzer' ],
444
445 // getGenderCache() provides a mock that considers first
446 // names ending in "a" to be female.
447 [ NS_USER, 'Lisa Müller', 'de', 'Benutzerin' ],
448 ];
449 }
450
451 /**
452 * @dataProvider provideGetNamespaceName
453 */
454 public function testGetNamespaceName( $namespace, $text, $lang, $expected ) {
455 $codec = $this->makeCodec( $lang );
456 $name = $codec->getNamespaceName( $namespace, $text );
457
458 $this->assertEquals( $expected, $name );
459 }
460 }