Merge "Set relevant title on Special:RecentChangesLinked"
[lhc/web/wiklou.git] / tests / phpunit / includes / TitleTest.php
1 <?php
2
3 /**
4 * @group Database
5 * ^--- needed for language cache stuff
6 *
7 * @group Title
8 */
9 class TitleTest extends MediaWikiTestCase {
10 protected function setUp() {
11 parent::setUp();
12
13 $this->setMwGlobals( array(
14 'wgLanguageCode' => 'en',
15 'wgContLang' => Language::factory( 'en' ),
16 // User language
17 'wgLang' => Language::factory( 'en' ),
18 'wgAllowUserJs' => false,
19 'wgDefaultLanguageVariant' => false,
20 ) );
21 }
22
23 public function addDBData() {
24 $this->db->replace( 'interwiki', 'iw_prefix',
25 array(
26 'iw_prefix' => 'externalwiki',
27 'iw_url' => '//example.com/$1',
28 'iw_api' => '//example.com/api.php',
29 'iw_wikiid' => '',
30 'iw_local' => 0,
31 'iw_trans' => 0,
32 )
33 );
34 }
35
36 /**
37 * @covers Title::legalChars
38 */
39 public function testLegalChars() {
40 $titlechars = Title::legalChars();
41
42 foreach ( range( 1, 255 ) as $num ) {
43 $chr = chr( $num );
44 if ( strpos( "#[]{}<>|", $chr ) !== false || preg_match( "/[\\x00-\\x1f\\x7f]/", $chr ) ) {
45 $this->assertFalse(
46 (bool)preg_match( "/[$titlechars]/", $chr ),
47 "chr($num) = $chr is not a valid titlechar"
48 );
49 } else {
50 $this->assertTrue(
51 (bool)preg_match( "/[$titlechars]/", $chr ),
52 "chr($num) = $chr is a valid titlechar"
53 );
54 }
55 }
56 }
57
58 /**
59 * See also mediawiki.Title.test.js
60 * @covers Title::secureAndSplit
61 * @todo This method should be split into 2 separate tests each with a provider
62 * @note This mainly tests MediaWikiTitleCodec::parseTitle().
63 */
64 public function testSecureAndSplit() {
65 $this->setMwGlobals( array(
66 'wgLocalInterwikis' => array( 'localtestiw' ),
67 'wgHooks' => array(
68 'InterwikiLoadPrefix' => array(
69 function ( $prefix, &$data ) {
70 if ( $prefix === 'localtestiw' ) {
71 $data = array( 'iw_url' => 'localtestiw' );
72 } elseif ( $prefix === 'remotetestiw' ) {
73 $data = array( 'iw_url' => 'remotetestiw' );
74 }
75 return false;
76 }
77 )
78 )
79 ));
80 // Valid
81 foreach ( array(
82 'Sandbox',
83 'A "B"',
84 'A \'B\'',
85 '.com',
86 '~',
87 '#',
88 '"',
89 '\'',
90 'Talk:Sandbox',
91 'Talk:Foo:Sandbox',
92 'File:Example.svg',
93 'File_talk:Example.svg',
94 'Foo/.../Sandbox',
95 'Sandbox/...',
96 'A~~',
97 ':A',
98 // Length is 256 total, but only title part matters
99 'Category:' . str_repeat( 'x', 248 ),
100 str_repeat( 'x', 252 ),
101 // interwiki prefix
102 'localtestiw: #anchor',
103 'localtestiw:',
104 'localtestiw:foo',
105 'localtestiw: foo # anchor',
106 'localtestiw: Talk: Sandbox # anchor',
107 'remotetestiw:',
108 'remotetestiw: Talk: # anchor',
109 'remotetestiw: #bar',
110 'remotetestiw: Talk:',
111 'remotetestiw: Talk: Foo',
112 'localtestiw:remotetestiw:',
113 'localtestiw:remotetestiw:foo'
114 ) as $text ) {
115 $this->assertInstanceOf( 'Title', Title::newFromText( $text ), "Valid: $text" );
116 }
117
118 // Invalid
119 foreach ( array(
120 '',
121 ':',
122 '__ __',
123 ' __ ',
124 // Bad characters forbidden regardless of wgLegalTitleChars
125 'A [ B',
126 'A ] B',
127 'A { B',
128 'A } B',
129 'A < B',
130 'A > B',
131 'A | B',
132 // URL encoding
133 'A%20B',
134 'A%23B',
135 'A%2523B',
136 // XML/HTML character entity references
137 // Note: Commented out because they are not marked invalid by the PHP test as
138 // Title::newFromText runs Sanitizer::decodeCharReferencesAndNormalize first.
139 //'A &eacute; B',
140 //'A &#233; B',
141 //'A &#x00E9; B',
142 // Subject of NS_TALK does not roundtrip to NS_MAIN
143 'Talk:File:Example.svg',
144 // Directory navigation
145 '.',
146 '..',
147 './Sandbox',
148 '../Sandbox',
149 'Foo/./Sandbox',
150 'Foo/../Sandbox',
151 'Sandbox/.',
152 'Sandbox/..',
153 // Tilde
154 'A ~~~ Name',
155 'A ~~~~ Signature',
156 'A ~~~~~ Timestamp',
157 str_repeat( 'x', 256 ),
158 // Namespace prefix without actual title
159 'Talk:',
160 'Talk:#',
161 'Category: ',
162 'Category: #bar',
163 // interwiki prefix
164 'localtestiw: Talk: # anchor',
165 'localtestiw: Talk:'
166 ) as $text ) {
167 $this->assertNull( Title::newFromText( $text ), "Invalid: $text" );
168 }
169 }
170
171 public static function provideConvertByteClassToUnicodeClass() {
172 return array(
173 array(
174 ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+',
175 ' %!"$&\'()*,\\-./0-9:;=?@A-Z\\\\\\^_`a-z~+\\u0080-\\uFFFF',
176 ),
177 array(
178 'QWERTYf-\\xFF+',
179 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
180 ),
181 array(
182 'QWERTY\\x66-\\xFD+',
183 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
184 ),
185 array(
186 'QWERTYf-y+',
187 'QWERTYf-y+',
188 ),
189 array(
190 'QWERTYf-\\x80+',
191 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
192 ),
193 array(
194 'QWERTY\\x66-\\x80+\\x23',
195 'QWERTYf-\\x7F+#\\u0080-\\uFFFF',
196 ),
197 array(
198 'QWERTY\\x66-\\x80+\\xD3',
199 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
200 ),
201 array(
202 '\\\\\\x99',
203 '\\\\\\u0080-\\uFFFF',
204 ),
205 array(
206 '-\\x99',
207 '\\-\\u0080-\\uFFFF',
208 ),
209 array(
210 'QWERTY\\-\\x99',
211 'QWERTY\\-\\u0080-\\uFFFF',
212 ),
213 array(
214 '\\\\x99',
215 '\\\\x99',
216 ),
217 array(
218 'A-\\x9F',
219 'A-\\x7F\\u0080-\\uFFFF',
220 ),
221 array(
222 '\\x66-\\x77QWERTY\\x88-\\x91FXZ',
223 'f-wQWERTYFXZ\\u0080-\\uFFFF',
224 ),
225 array(
226 '\\x66-\\x99QWERTY\\xAA-\\xEEFXZ',
227 'f-\\x7FQWERTYFXZ\\u0080-\\uFFFF',
228 ),
229 );
230 }
231
232 /**
233 * @dataProvider provideConvertByteClassToUnicodeClass
234 * @covers Title::convertByteClassToUnicodeClass
235 */
236 public function testConvertByteClassToUnicodeClass( $byteClass, $unicodeClass ) {
237 $this->assertEquals( $unicodeClass, Title::convertByteClassToUnicodeClass( $byteClass ) );
238 }
239
240 /**
241 * @dataProvider provideBug31100
242 * @covers Title::fixSpecialName
243 * @todo give this test a real name explaining what is being tested here
244 */
245 public function testBug31100FixSpecialName( $text, $expectedParam ) {
246 $title = Title::newFromText( $text );
247 $fixed = $title->fixSpecialName();
248 $stuff = explode( '/', $fixed->getDBkey(), 2 );
249 if ( count( $stuff ) == 2 ) {
250 $par = $stuff[1];
251 } else {
252 $par = null;
253 }
254 $this->assertEquals(
255 $expectedParam,
256 $par,
257 "Bug 31100 regression check: Title->fixSpecialName() should preserve parameter"
258 );
259 }
260
261 public static function provideBug31100() {
262 return array(
263 array( 'Special:Version', null ),
264 array( 'Special:Version/', '' ),
265 array( 'Special:Version/param', 'param' ),
266 );
267 }
268
269 /**
270 * Auth-less test of Title::isValidMoveOperation
271 *
272 * @group Database
273 * @param string $source
274 * @param string $target
275 * @param array|string|bool $expected Required error
276 * @dataProvider provideTestIsValidMoveOperation
277 * @covers Title::isValidMoveOperation
278 */
279 public function testIsValidMoveOperation( $source, $target, $expected ) {
280 $title = Title::newFromText( $source );
281 $nt = Title::newFromText( $target );
282 $errors = $title->isValidMoveOperation( $nt, false );
283 if ( $expected === true ) {
284 $this->assertTrue( $errors );
285 } else {
286 $errors = $this->flattenErrorsArray( $errors );
287 foreach ( (array)$expected as $error ) {
288 $this->assertContains( $error, $errors );
289 }
290 }
291 }
292
293 /**
294 * Provides test parameter values for testIsValidMoveOperation()
295 */
296 public function dataTestIsValidMoveOperation() {
297 return array(
298 array( 'Test', 'Test', 'selfmove' ),
299 array( 'File:Test.jpg', 'Page', 'imagenocrossnamespace' )
300 );
301 }
302
303 /**
304 * Auth-less test of Title::userCan
305 *
306 * @param array $whitelistRegexp
307 * @param string $source
308 * @param string $action
309 * @param array|string|bool $expected Required error
310 *
311 * @covers Title::checkReadPermissions
312 * @dataProvider dataWgWhitelistReadRegexp
313 */
314 public function testWgWhitelistReadRegexp( $whitelistRegexp, $source, $action, $expected ) {
315 // $wgWhitelistReadRegexp must be an array. Since the provided test cases
316 // usually have only one regex, it is more concise to write the lonely regex
317 // as a string. Thus we cast to an array() to honor $wgWhitelistReadRegexp
318 // type requisite.
319 if ( is_string( $whitelistRegexp ) ) {
320 $whitelistRegexp = array( $whitelistRegexp );
321 }
322
323 $title = Title::newFromDBkey( $source );
324
325 global $wgGroupPermissions;
326 $oldPermissions = $wgGroupPermissions;
327 // Disallow all so we can ensure our regex works
328 $wgGroupPermissions = array();
329 $wgGroupPermissions['*']['read'] = false;
330
331 global $wgWhitelistRead;
332 $oldWhitelist = $wgWhitelistRead;
333 // Undo any LocalSettings explicite whitelists so they won't cause a
334 // failing test to succeed. Set it to some random non sense just
335 // to make sure we properly test Title::checkReadPermissions()
336 $wgWhitelistRead = array( 'some random non sense title' );
337
338 global $wgWhitelistReadRegexp;
339 $oldWhitelistRegexp = $wgWhitelistReadRegexp;
340 $wgWhitelistReadRegexp = $whitelistRegexp;
341
342 // Just use $wgUser which in test is a user object for '127.0.0.1'
343 global $wgUser;
344 // Invalidate user rights cache to take in account $wgGroupPermissions
345 // change above.
346 $wgUser->clearInstanceCache();
347 $errors = $title->userCan( $action, $wgUser );
348
349 // Restore globals
350 $wgGroupPermissions = $oldPermissions;
351 $wgWhitelistRead = $oldWhitelist;
352 $wgWhitelistReadRegexp = $oldWhitelistRegexp;
353
354 if ( is_bool( $expected ) ) {
355 # Forge the assertion message depending on the assertion expectation
356 $allowableness = $expected
357 ? " should be allowed"
358 : " should NOT be allowed";
359 $this->assertEquals(
360 $expected,
361 $errors,
362 "User action '$action' on [[$source]] $allowableness."
363 );
364 } else {
365 $errors = $this->flattenErrorsArray( $errors );
366 foreach ( (array)$expected as $error ) {
367 $this->assertContains( $error, $errors );
368 }
369 }
370 }
371
372 /**
373 * Provides test parameter values for testWgWhitelistReadRegexp()
374 */
375 public function dataWgWhitelistReadRegexp() {
376 $ALLOWED = true;
377 $DISALLOWED = false;
378
379 return array(
380 // Everything, if this doesn't work, we're really in trouble
381 array( '/.*/', 'Main_Page', 'read', $ALLOWED ),
382 array( '/.*/', 'Main_Page', 'edit', $DISALLOWED ),
383
384 // We validate against the title name, not the db key
385 array( '/^Main_Page$/', 'Main_Page', 'read', $DISALLOWED ),
386 // Main page
387 array( '/^Main/', 'Main_Page', 'read', $ALLOWED ),
388 array( '/^Main.*/', 'Main_Page', 'read', $ALLOWED ),
389 // With spaces
390 array( '/Mic\sCheck/', 'Mic Check', 'read', $ALLOWED ),
391 // Unicode multibyte
392 // ...without unicode modifier
393 array( '/Unicode Test . Yes/', 'Unicode Test Ñ Yes', 'read', $DISALLOWED ),
394 // ...with unicode modifier
395 array( '/Unicode Test . Yes/u', 'Unicode Test Ñ Yes', 'read', $ALLOWED ),
396 // Case insensitive
397 array( '/MiC ChEcK/', 'mic check', 'read', $DISALLOWED ),
398 array( '/MiC ChEcK/i', 'mic check', 'read', $ALLOWED ),
399
400 // From DefaultSettings.php:
401 array( "@^UsEr.*@i", 'User is banned', 'read', $ALLOWED ),
402 array( "@^UsEr.*@i", 'User:John Doe', 'read', $ALLOWED ),
403
404 // With namespaces:
405 array( '/^Special:NewPages$/', 'Special:NewPages', 'read', $ALLOWED ),
406 array( null, 'Special:Newpages', 'read', $DISALLOWED ),
407
408 );
409 }
410
411 public function flattenErrorsArray( $errors ) {
412 $result = array();
413 foreach ( $errors as $error ) {
414 $result[] = $error[0];
415 }
416
417 return $result;
418 }
419
420 public static function provideTestIsValidMoveOperation() {
421 return array(
422 array( 'Test', 'Test', 'selfmove' ),
423 array( 'File:Test.jpg', 'Page', 'imagenocrossnamespace' )
424 );
425 }
426
427 /**
428 * @dataProvider provideGetPageViewLanguage
429 * @covers Title::getPageViewLanguage
430 */
431 public function testGetPageViewLanguage( $expected, $titleText, $contLang,
432 $lang, $variant, $msg = ''
433 ) {
434 global $wgLanguageCode, $wgContLang, $wgLang, $wgDefaultLanguageVariant, $wgAllowUserJs;
435
436 // Setup environnement for this test
437 $wgLanguageCode = $contLang;
438 $wgContLang = Language::factory( $contLang );
439 $wgLang = Language::factory( $lang );
440 $wgDefaultLanguageVariant = $variant;
441 $wgAllowUserJs = true;
442
443 $title = Title::newFromText( $titleText );
444 $this->assertInstanceOf( 'Title', $title,
445 "Test must be passed a valid title text, you gave '$titleText'"
446 );
447 $this->assertEquals( $expected,
448 $title->getPageViewLanguage()->getCode(),
449 $msg
450 );
451 }
452
453 public static function provideGetPageViewLanguage() {
454 # Format:
455 # - expected
456 # - Title name
457 # - wgContLang (expected in most case)
458 # - wgLang (on some specific pages)
459 # - wgDefaultLanguageVariant
460 # - Optional message
461 return array(
462 array( 'fr', 'Help:I_need_somebody', 'fr', 'fr', false ),
463 array( 'es', 'Help:I_need_somebody', 'es', 'zh-tw', false ),
464 array( 'zh', 'Help:I_need_somebody', 'zh', 'zh-tw', false ),
465
466 array( 'es', 'Help:I_need_somebody', 'es', 'zh-tw', 'zh-cn' ),
467 array( 'es', 'MediaWiki:About', 'es', 'zh-tw', 'zh-cn' ),
468 array( 'es', 'MediaWiki:About/', 'es', 'zh-tw', 'zh-cn' ),
469 array( 'de', 'MediaWiki:About/de', 'es', 'zh-tw', 'zh-cn' ),
470 array( 'en', 'MediaWiki:Common.js', 'es', 'zh-tw', 'zh-cn' ),
471 array( 'en', 'MediaWiki:Common.css', 'es', 'zh-tw', 'zh-cn' ),
472 array( 'en', 'User:JohnDoe/Common.js', 'es', 'zh-tw', 'zh-cn' ),
473 array( 'en', 'User:JohnDoe/Monobook.css', 'es', 'zh-tw', 'zh-cn' ),
474
475 array( 'zh-cn', 'Help:I_need_somebody', 'zh', 'zh-tw', 'zh-cn' ),
476 array( 'zh', 'MediaWiki:About', 'zh', 'zh-tw', 'zh-cn' ),
477 array( 'zh', 'MediaWiki:About/', 'zh', 'zh-tw', 'zh-cn' ),
478 array( 'de', 'MediaWiki:About/de', 'zh', 'zh-tw', 'zh-cn' ),
479 array( 'zh-cn', 'MediaWiki:About/zh-cn', 'zh', 'zh-tw', 'zh-cn' ),
480 array( 'zh-tw', 'MediaWiki:About/zh-tw', 'zh', 'zh-tw', 'zh-cn' ),
481 array( 'en', 'MediaWiki:Common.js', 'zh', 'zh-tw', 'zh-cn' ),
482 array( 'en', 'MediaWiki:Common.css', 'zh', 'zh-tw', 'zh-cn' ),
483 array( 'en', 'User:JohnDoe/Common.js', 'zh', 'zh-tw', 'zh-cn' ),
484 array( 'en', 'User:JohnDoe/Monobook.css', 'zh', 'zh-tw', 'zh-cn' ),
485
486 array( 'zh-tw', 'Special:NewPages', 'es', 'zh-tw', 'zh-cn' ),
487 array( 'zh-tw', 'Special:NewPages', 'zh', 'zh-tw', 'zh-cn' ),
488
489 );
490 }
491
492 /**
493 * @dataProvider provideBaseTitleCases
494 * @covers Title::getBaseText
495 */
496 public function testGetBaseText( $title, $expected, $msg = '' ) {
497 $title = Title::newFromText( $title );
498 $this->assertEquals( $expected,
499 $title->getBaseText(),
500 $msg
501 );
502 }
503
504 public static function provideBaseTitleCases() {
505 return array(
506 # Title, expected base, optional message
507 array( 'User:John_Doe/subOne/subTwo', 'John Doe/subOne' ),
508 array( 'User:Foo/Bar/Baz', 'Foo/Bar' ),
509 );
510 }
511
512 /**
513 * @dataProvider provideRootTitleCases
514 * @covers Title::getRootText
515 */
516 public function testGetRootText( $title, $expected, $msg = '' ) {
517 $title = Title::newFromText( $title );
518 $this->assertEquals( $expected,
519 $title->getRootText(),
520 $msg
521 );
522 }
523
524 public static function provideRootTitleCases() {
525 return array(
526 # Title, expected base, optional message
527 array( 'User:John_Doe/subOne/subTwo', 'John Doe' ),
528 array( 'User:Foo/Bar/Baz', 'Foo' ),
529 );
530 }
531
532 /**
533 * @todo Handle $wgNamespacesWithSubpages cases
534 * @dataProvider provideSubpageTitleCases
535 * @covers Title::getSubpageText
536 */
537 public function testGetSubpageText( $title, $expected, $msg = '' ) {
538 $title = Title::newFromText( $title );
539 $this->assertEquals( $expected,
540 $title->getSubpageText(),
541 $msg
542 );
543 }
544
545 public static function provideSubpageTitleCases() {
546 return array(
547 # Title, expected base, optional message
548 array( 'User:John_Doe/subOne/subTwo', 'subTwo' ),
549 array( 'User:John_Doe/subOne', 'subOne' ),
550 );
551 }
552
553 public function provideNewFromTitleValue() {
554 return array(
555 array( new TitleValue( NS_MAIN, 'Foo' ) ),
556 array( new TitleValue( NS_MAIN, 'Foo', 'bar' ) ),
557 array( new TitleValue( NS_USER, 'Hansi_Maier' ) ),
558 );
559 }
560
561 /**
562 * @dataProvider provideNewFromTitleValue
563 */
564 public function testNewFromTitleValue( TitleValue $value ) {
565 $title = Title::newFromTitleValue( $value );
566
567 $dbkey = str_replace( ' ', '_', $value->getText() );
568 $this->assertEquals( $dbkey, $title->getDBkey() );
569 $this->assertEquals( $value->getNamespace(), $title->getNamespace() );
570 $this->assertEquals( $value->getFragment(), $title->getFragment() );
571 }
572
573 public function provideGetTitleValue() {
574 return array(
575 array( 'Foo' ),
576 array( 'Foo#bar' ),
577 array( 'User:Hansi_Maier' ),
578 );
579 }
580
581 /**
582 * @dataProvider provideGetTitleValue
583 */
584 public function testGetTitleValue( $text ) {
585 $title = Title::newFromText( $text );
586 $value = $title->getTitleValue();
587
588 $dbkey = str_replace( ' ', '_', $value->getText() );
589 $this->assertEquals( $title->getDBkey(), $dbkey );
590 $this->assertEquals( $title->getNamespace(), $value->getNamespace() );
591 $this->assertEquals( $title->getFragment(), $value->getFragment() );
592 }
593
594 public function provideGetFragment() {
595 return array(
596 array( 'Foo', '' ),
597 array( 'Foo#bar', 'bar' ),
598 array( 'Foo#bär', 'bär' ),
599
600 // Inner whitespace is normalized
601 array( 'Foo#bar_bar', 'bar bar' ),
602 array( 'Foo#bar bar', 'bar bar' ),
603 array( 'Foo#bar bar', 'bar bar' ),
604
605 // Leading whitespace is kept, trailing whitespace is trimmed.
606 // XXX: Is this really want we want?
607 array( 'Foo#_bar_bar_', ' bar bar' ),
608 array( 'Foo# bar bar ', ' bar bar' ),
609 );
610 }
611
612 /**
613 * @dataProvider provideGetFragment
614 *
615 * @param string $full
616 * @param string $fragment
617 */
618 public function testGetFragment( $full, $fragment ) {
619 $title = Title::newFromText( $full );
620 $this->assertEquals( $fragment, $title->getFragment() );
621 }
622
623 /**
624 * @covers Title::isAlwaysKnown
625 * @dataProvider provideIsAlwaysKnown
626 * @param string $page
627 * @param bool $isKnown
628 */
629 public function testIsAlwaysKnown( $page, $isKnown ) {
630 $title = Title::newFromText( $page );
631 $this->assertEquals( $isKnown, $title->isAlwaysKnown() );
632 }
633
634 public function provideIsAlwaysKnown() {
635 return array(
636 array( 'Some nonexistent page', false ),
637 array( 'UTPage', false ),
638 array( '#test', true ),
639 array( 'Special:BlankPage', true ),
640 array( 'Special:SomeNonexistentSpecialPage', false ),
641 array( 'MediaWiki:Parentheses', true ),
642 array( 'MediaWiki:Some nonexistent message', false ),
643 array( 'externalwiki:Interwiki link', true ),
644 );
645 }
646 }