03802a85ecc87f6d17b0f5e9c21cb19738318d32
[lhc/web/wiklou.git] / tests / phpunit / includes / TitleTest.php
1 <?php
2
3 use MediaWiki\Linker\LinkTarget;
4 use MediaWiki\MediaWikiServices;
5
6 /**
7 * @group Database
8 * @group Title
9 */
10 class TitleTest extends MediaWikiTestCase {
11 protected function setUp() {
12 parent::setUp();
13
14 $this->setMwGlobals( [
15 'wgAllowUserJs' => false,
16 'wgDefaultLanguageVariant' => false,
17 'wgMetaNamespace' => 'Project',
18 ] );
19 $this->setUserLang( 'en' );
20 $this->setContentLang( 'en' );
21 }
22
23 /**
24 * @covers Title::legalChars
25 */
26 public function testLegalChars() {
27 $titlechars = Title::legalChars();
28
29 foreach ( range( 1, 255 ) as $num ) {
30 $chr = chr( $num );
31 if ( strpos( "#[]{}<>|", $chr ) !== false || preg_match( "/[\\x00-\\x1f\\x7f]/", $chr ) ) {
32 $this->assertFalse(
33 (bool)preg_match( "/[$titlechars]/", $chr ),
34 "chr($num) = $chr is not a valid titlechar"
35 );
36 } else {
37 $this->assertTrue(
38 (bool)preg_match( "/[$titlechars]/", $chr ),
39 "chr($num) = $chr is a valid titlechar"
40 );
41 }
42 }
43 }
44
45 public static function provideValidSecureAndSplit() {
46 return [
47 [ 'Sandbox' ],
48 [ 'A "B"' ],
49 [ 'A \'B\'' ],
50 [ '.com' ],
51 [ '~' ],
52 [ '#' ],
53 [ '"' ],
54 [ '\'' ],
55 [ 'Talk:Sandbox' ],
56 [ 'Talk:Foo:Sandbox' ],
57 [ 'File:Example.svg' ],
58 [ 'File_talk:Example.svg' ],
59 [ 'Foo/.../Sandbox' ],
60 [ 'Sandbox/...' ],
61 [ 'A~~' ],
62 [ ':A' ],
63 // Length is 256 total, but only title part matters
64 [ 'Category:' . str_repeat( 'x', 248 ) ],
65 [ str_repeat( 'x', 252 ) ],
66 // interwiki prefix
67 [ 'localtestiw: #anchor' ],
68 [ 'localtestiw:' ],
69 [ 'localtestiw:foo' ],
70 [ 'localtestiw: foo # anchor' ],
71 [ 'localtestiw: Talk: Sandbox # anchor' ],
72 [ 'remotetestiw:' ],
73 [ 'remotetestiw: Talk: # anchor' ],
74 [ 'remotetestiw: #bar' ],
75 [ 'remotetestiw: Talk:' ],
76 [ 'remotetestiw: Talk: Foo' ],
77 [ 'localtestiw:remotetestiw:' ],
78 [ 'localtestiw:remotetestiw:foo' ]
79 ];
80 }
81
82 public static function provideInvalidSecureAndSplit() {
83 return [
84 [ '', 'title-invalid-empty' ],
85 [ ':', 'title-invalid-empty' ],
86 [ '__ __', 'title-invalid-empty' ],
87 [ ' __ ', 'title-invalid-empty' ],
88 // Bad characters forbidden regardless of wgLegalTitleChars
89 [ 'A [ B', 'title-invalid-characters' ],
90 [ 'A ] B', 'title-invalid-characters' ],
91 [ 'A { B', 'title-invalid-characters' ],
92 [ 'A } B', 'title-invalid-characters' ],
93 [ 'A < B', 'title-invalid-characters' ],
94 [ 'A > B', 'title-invalid-characters' ],
95 [ 'A | B', 'title-invalid-characters' ],
96 [ "A \t B", 'title-invalid-characters' ],
97 [ "A \n B", 'title-invalid-characters' ],
98 // URL encoding
99 [ 'A%20B', 'title-invalid-characters' ],
100 [ 'A%23B', 'title-invalid-characters' ],
101 [ 'A%2523B', 'title-invalid-characters' ],
102 // XML/HTML character entity references
103 // Note: Commented out because they are not marked invalid by the PHP test as
104 // Title::newFromText runs Sanitizer::decodeCharReferencesAndNormalize first.
105 // 'A &eacute; B',
106 // 'A &#233; B',
107 // 'A &#x00E9; B',
108 // Subject of NS_TALK does not roundtrip to NS_MAIN
109 [ 'Talk:File:Example.svg', 'title-invalid-talk-namespace' ],
110 // Directory navigation
111 [ '.', 'title-invalid-relative' ],
112 [ '..', 'title-invalid-relative' ],
113 [ './Sandbox', 'title-invalid-relative' ],
114 [ '../Sandbox', 'title-invalid-relative' ],
115 [ 'Foo/./Sandbox', 'title-invalid-relative' ],
116 [ 'Foo/../Sandbox', 'title-invalid-relative' ],
117 [ 'Sandbox/.', 'title-invalid-relative' ],
118 [ 'Sandbox/..', 'title-invalid-relative' ],
119 // Tilde
120 [ 'A ~~~ Name', 'title-invalid-magic-tilde' ],
121 [ 'A ~~~~ Signature', 'title-invalid-magic-tilde' ],
122 [ 'A ~~~~~ Timestamp', 'title-invalid-magic-tilde' ],
123 // Length
124 [ str_repeat( 'x', 256 ), 'title-invalid-too-long' ],
125 // Namespace prefix without actual title
126 [ 'Talk:', 'title-invalid-empty' ],
127 [ 'Talk:#', 'title-invalid-empty' ],
128 [ 'Category: ', 'title-invalid-empty' ],
129 [ 'Category: #bar', 'title-invalid-empty' ],
130 // interwiki prefix
131 [ 'localtestiw: Talk: # anchor', 'title-invalid-empty' ],
132 [ 'localtestiw: Talk:', 'title-invalid-empty' ]
133 ];
134 }
135
136 private function secureAndSplitGlobals() {
137 $this->setMwGlobals( [
138 'wgLocalInterwikis' => [ 'localtestiw' ],
139 'wgHooks' => [
140 'InterwikiLoadPrefix' => [
141 function ( $prefix, &$data ) {
142 if ( $prefix === 'localtestiw' ) {
143 $data = [ 'iw_url' => 'localtestiw' ];
144 } elseif ( $prefix === 'remotetestiw' ) {
145 $data = [ 'iw_url' => 'remotetestiw' ];
146 }
147 return false;
148 }
149 ]
150 ]
151 ] );
152
153 // Reset TitleParser since we modified $wgLocalInterwikis
154 $this->setService( 'TitleParser', new MediaWikiTitleCodec(
155 Language::factory( 'en' ),
156 new GenderCache(),
157 [ 'localtestiw' ]
158 ) );
159 }
160
161 /**
162 * See also mediawiki.Title.test.js
163 * @covers Title::secureAndSplit
164 * @dataProvider provideValidSecureAndSplit
165 * @note This mainly tests MediaWikiTitleCodec::parseTitle().
166 */
167 public function testSecureAndSplitValid( $text ) {
168 $this->secureAndSplitGlobals();
169 $this->assertInstanceOf( Title::class, Title::newFromText( $text ), "Valid: $text" );
170 }
171
172 /**
173 * See also mediawiki.Title.test.js
174 * @covers Title::secureAndSplit
175 * @dataProvider provideInvalidSecureAndSplit
176 * @note This mainly tests MediaWikiTitleCodec::parseTitle().
177 */
178 public function testSecureAndSplitInvalid( $text, $expectedErrorMessage ) {
179 $this->secureAndSplitGlobals();
180 try {
181 Title::newFromTextThrow( $text ); // should throw
182 $this->assertTrue( false, "Invalid: $text" );
183 } catch ( MalformedTitleException $ex ) {
184 $this->assertEquals( $expectedErrorMessage, $ex->getErrorMessage(), "Invalid: $text" );
185 }
186 }
187
188 public static function provideConvertByteClassToUnicodeClass() {
189 return [
190 [
191 ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+',
192 ' %!"$&\'()*,\\-./0-9:;=?@A-Z\\\\\\^_`a-z~+\\u0080-\\uFFFF',
193 ],
194 [
195 'QWERTYf-\\xFF+',
196 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
197 ],
198 [
199 'QWERTY\\x66-\\xFD+',
200 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
201 ],
202 [
203 'QWERTYf-y+',
204 'QWERTYf-y+',
205 ],
206 [
207 'QWERTYf-\\x80+',
208 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
209 ],
210 [
211 'QWERTY\\x66-\\x80+\\x23',
212 'QWERTYf-\\x7F+#\\u0080-\\uFFFF',
213 ],
214 [
215 'QWERTY\\x66-\\x80+\\xD3',
216 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
217 ],
218 [
219 '\\\\\\x99',
220 '\\\\\\u0080-\\uFFFF',
221 ],
222 [
223 '-\\x99',
224 '\\-\\u0080-\\uFFFF',
225 ],
226 [
227 'QWERTY\\-\\x99',
228 'QWERTY\\-\\u0080-\\uFFFF',
229 ],
230 [
231 '\\\\x99',
232 '\\\\x99',
233 ],
234 [
235 'A-\\x9F',
236 'A-\\x7F\\u0080-\\uFFFF',
237 ],
238 [
239 '\\x66-\\x77QWERTY\\x88-\\x91FXZ',
240 'f-wQWERTYFXZ\\u0080-\\uFFFF',
241 ],
242 [
243 '\\x66-\\x99QWERTY\\xAA-\\xEEFXZ',
244 'f-\\x7FQWERTYFXZ\\u0080-\\uFFFF',
245 ],
246 ];
247 }
248
249 /**
250 * @dataProvider provideConvertByteClassToUnicodeClass
251 * @covers Title::convertByteClassToUnicodeClass
252 */
253 public function testConvertByteClassToUnicodeClass( $byteClass, $unicodeClass ) {
254 $this->assertEquals( $unicodeClass, Title::convertByteClassToUnicodeClass( $byteClass ) );
255 }
256
257 /**
258 * @dataProvider provideSpecialNamesWithAndWithoutParameter
259 * @covers Title::fixSpecialName
260 */
261 public function testFixSpecialNameRetainsParameter( $text, $expectedParam ) {
262 $title = Title::newFromText( $text );
263 $fixed = $title->fixSpecialName();
264 $stuff = explode( '/', $fixed->getDBkey(), 2 );
265 if ( count( $stuff ) == 2 ) {
266 $par = $stuff[1];
267 } else {
268 $par = null;
269 }
270 $this->assertEquals(
271 $expectedParam,
272 $par,
273 "T33100 regression check: Title->fixSpecialName() should preserve parameter"
274 );
275 }
276
277 public static function provideSpecialNamesWithAndWithoutParameter() {
278 return [
279 [ 'Special:Version', null ],
280 [ 'Special:Version/', '' ],
281 [ 'Special:Version/param', 'param' ],
282 ];
283 }
284
285 /**
286 * Auth-less test of Title::isValidMoveOperation
287 *
288 * @param string $source
289 * @param string $target
290 * @param array|string|bool $expected Required error
291 * @dataProvider provideTestIsValidMoveOperation
292 * @covers Title::isValidMoveOperation
293 */
294 public function testIsValidMoveOperation( $source, $target, $expected ) {
295 $this->setMwGlobals( 'wgContentHandlerUseDB', false );
296 $title = Title::newFromText( $source );
297 $nt = Title::newFromText( $target );
298 $errors = $title->isValidMoveOperation( $nt, false );
299 if ( $expected === true ) {
300 $this->assertTrue( $errors );
301 } else {
302 $errors = $this->flattenErrorsArray( $errors );
303 foreach ( (array)$expected as $error ) {
304 $this->assertContains( $error, $errors );
305 }
306 }
307 }
308
309 public static function provideTestIsValidMoveOperation() {
310 return [
311 // for Title::isValidMoveOperation
312 [ 'Some page', '', 'badtitletext' ],
313 [ 'Test', 'Test', 'selfmove' ],
314 [ 'Special:FooBar', 'Test', 'immobile-source-namespace' ],
315 [ 'Test', 'Special:FooBar', 'immobile-target-namespace' ],
316 [ 'MediaWiki:Common.js', 'Help:Some wikitext page', 'bad-target-model' ],
317 [ 'Page', 'File:Test.jpg', 'nonfile-cannot-move-to-file' ],
318 [ 'File:Test.jpg', 'Page', 'imagenocrossnamespace' ],
319 ];
320 }
321
322 /**
323 * Auth-less test of Title::userCan
324 *
325 * @param array $whitelistRegexp
326 * @param string $source
327 * @param string $action
328 * @param array|string|bool $expected Required error
329 *
330 * @covers Title::checkReadPermissions
331 * @dataProvider dataWgWhitelistReadRegexp
332 */
333 public function testWgWhitelistReadRegexp( $whitelistRegexp, $source, $action, $expected ) {
334 // $wgWhitelistReadRegexp must be an array. Since the provided test cases
335 // usually have only one regex, it is more concise to write the lonely regex
336 // as a string. Thus we cast to an array() to honor $wgWhitelistReadRegexp
337 // type requisite.
338 if ( is_string( $whitelistRegexp ) ) {
339 $whitelistRegexp = [ $whitelistRegexp ];
340 }
341
342 $this->setMwGlobals( [
343 // So User::isEveryoneAllowed( 'read' ) === false
344 'wgGroupPermissions' => [ '*' => [ 'read' => false ] ],
345 'wgWhitelistRead' => [ 'some random non sense title' ],
346 'wgWhitelistReadRegexp' => $whitelistRegexp,
347 ] );
348
349 $title = Title::newFromDBkey( $source );
350
351 // New anonymous user with no rights
352 $user = new User;
353 $user->mRights = [];
354 $errors = $title->userCan( $action, $user );
355
356 if ( is_bool( $expected ) ) {
357 # Forge the assertion message depending on the assertion expectation
358 $allowableness = $expected
359 ? " should be allowed"
360 : " should NOT be allowed";
361 $this->assertEquals(
362 $expected,
363 $errors,
364 "User action '$action' on [[$source]] $allowableness."
365 );
366 } else {
367 $errors = $this->flattenErrorsArray( $errors );
368 foreach ( (array)$expected as $error ) {
369 $this->assertContains( $error, $errors );
370 }
371 }
372 }
373
374 /**
375 * Provides test parameter values for testWgWhitelistReadRegexp()
376 */
377 public function dataWgWhitelistReadRegexp() {
378 $ALLOWED = true;
379 $DISALLOWED = false;
380
381 return [
382 // Everything, if this doesn't work, we're really in trouble
383 [ '/.*/', 'Main_Page', 'read', $ALLOWED ],
384 [ '/.*/', 'Main_Page', 'edit', $DISALLOWED ],
385
386 // We validate against the title name, not the db key
387 [ '/^Main_Page$/', 'Main_Page', 'read', $DISALLOWED ],
388 // Main page
389 [ '/^Main/', 'Main_Page', 'read', $ALLOWED ],
390 [ '/^Main.*/', 'Main_Page', 'read', $ALLOWED ],
391 // With spaces
392 [ '/Mic\sCheck/', 'Mic Check', 'read', $ALLOWED ],
393 // Unicode multibyte
394 // ...without unicode modifier
395 [ '/Unicode Test . Yes/', 'Unicode Test Ñ Yes', 'read', $DISALLOWED ],
396 // ...with unicode modifier
397 [ '/Unicode Test . Yes/u', 'Unicode Test Ñ Yes', 'read', $ALLOWED ],
398 // Case insensitive
399 [ '/MiC ChEcK/', 'mic check', 'read', $DISALLOWED ],
400 [ '/MiC ChEcK/i', 'mic check', 'read', $ALLOWED ],
401
402 // From DefaultSettings.php:
403 [ "@^UsEr.*@i", 'User is banned', 'read', $ALLOWED ],
404 [ "@^UsEr.*@i", 'User:John Doe', 'read', $ALLOWED ],
405
406 // With namespaces:
407 [ '/^Special:NewPages$/', 'Special:NewPages', 'read', $ALLOWED ],
408 [ null, 'Special:Newpages', 'read', $DISALLOWED ],
409
410 ];
411 }
412
413 public function flattenErrorsArray( $errors ) {
414 $result = [];
415 foreach ( $errors as $error ) {
416 $result[] = $error[0];
417 }
418
419 return $result;
420 }
421
422 /**
423 * @dataProvider provideGetPageViewLanguage
424 * @covers Title::getPageViewLanguage
425 */
426 public function testGetPageViewLanguage( $expected, $titleText, $contLang,
427 $lang, $variant, $msg = ''
428 ) {
429 // Setup environnement for this test
430 $this->setMwGlobals( [
431 'wgDefaultLanguageVariant' => $variant,
432 'wgAllowUserJs' => true,
433 ] );
434 $this->setUserLang( $lang );
435 $this->setContentLang( $contLang );
436
437 $title = Title::newFromText( $titleText );
438 $this->assertInstanceOf( Title::class, $title,
439 "Test must be passed a valid title text, you gave '$titleText'"
440 );
441 $this->assertEquals( $expected,
442 $title->getPageViewLanguage()->getCode(),
443 $msg
444 );
445 }
446
447 public static function provideGetPageViewLanguage() {
448 # Format:
449 # - expected
450 # - Title name
451 # - content language (expected in most cases)
452 # - wgLang (on some specific pages)
453 # - wgDefaultLanguageVariant
454 # - Optional message
455 return [
456 [ 'fr', 'Help:I_need_somebody', 'fr', 'fr', false ],
457 [ 'es', 'Help:I_need_somebody', 'es', 'zh-tw', false ],
458 [ 'zh', 'Help:I_need_somebody', 'zh', 'zh-tw', false ],
459
460 [ 'es', 'Help:I_need_somebody', 'es', 'zh-tw', 'zh-cn' ],
461 [ 'es', 'MediaWiki:About', 'es', 'zh-tw', 'zh-cn' ],
462 [ 'es', 'MediaWiki:About/', 'es', 'zh-tw', 'zh-cn' ],
463 [ 'de', 'MediaWiki:About/de', 'es', 'zh-tw', 'zh-cn' ],
464 [ 'en', 'MediaWiki:Common.js', 'es', 'zh-tw', 'zh-cn' ],
465 [ 'en', 'MediaWiki:Common.css', 'es', 'zh-tw', 'zh-cn' ],
466 [ 'en', 'User:JohnDoe/Common.js', 'es', 'zh-tw', 'zh-cn' ],
467 [ 'en', 'User:JohnDoe/Monobook.css', 'es', 'zh-tw', 'zh-cn' ],
468
469 [ 'zh-cn', 'Help:I_need_somebody', 'zh', 'zh-tw', 'zh-cn' ],
470 [ 'zh', 'MediaWiki:About', 'zh', 'zh-tw', 'zh-cn' ],
471 [ 'zh', 'MediaWiki:About/', 'zh', 'zh-tw', 'zh-cn' ],
472 [ 'de', 'MediaWiki:About/de', 'zh', 'zh-tw', 'zh-cn' ],
473 [ 'zh-cn', 'MediaWiki:About/zh-cn', 'zh', 'zh-tw', 'zh-cn' ],
474 [ 'zh-tw', 'MediaWiki:About/zh-tw', 'zh', 'zh-tw', 'zh-cn' ],
475 [ 'en', 'MediaWiki:Common.js', 'zh', 'zh-tw', 'zh-cn' ],
476 [ 'en', 'MediaWiki:Common.css', 'zh', 'zh-tw', 'zh-cn' ],
477 [ 'en', 'User:JohnDoe/Common.js', 'zh', 'zh-tw', 'zh-cn' ],
478 [ 'en', 'User:JohnDoe/Monobook.css', 'zh', 'zh-tw', 'zh-cn' ],
479
480 [ 'zh-tw', 'Special:NewPages', 'es', 'zh-tw', 'zh-cn' ],
481 [ 'zh-tw', 'Special:NewPages', 'zh', 'zh-tw', 'zh-cn' ],
482
483 ];
484 }
485
486 /**
487 * @dataProvider provideBaseTitleCases
488 * @covers Title::getBaseText
489 */
490 public function testGetBaseText( $title, $expected, $msg = '' ) {
491 $title = Title::newFromText( $title );
492 $this->assertEquals( $expected,
493 $title->getBaseText(),
494 $msg
495 );
496 }
497
498 public static function provideBaseTitleCases() {
499 return [
500 # Title, expected base, optional message
501 [ 'User:John_Doe/subOne/subTwo', 'John Doe/subOne' ],
502 [ 'User:Foo/Bar/Baz', 'Foo/Bar' ],
503 ];
504 }
505
506 /**
507 * @dataProvider provideRootTitleCases
508 * @covers Title::getRootText
509 */
510 public function testGetRootText( $title, $expected, $msg = '' ) {
511 $title = Title::newFromText( $title );
512 $this->assertEquals( $expected,
513 $title->getRootText(),
514 $msg
515 );
516 }
517
518 public static function provideRootTitleCases() {
519 return [
520 # Title, expected base, optional message
521 [ 'User:John_Doe/subOne/subTwo', 'John Doe' ],
522 [ 'User:Foo/Bar/Baz', 'Foo' ],
523 ];
524 }
525
526 /**
527 * @todo Handle $wgNamespacesWithSubpages cases
528 * @dataProvider provideSubpageTitleCases
529 * @covers Title::getSubpageText
530 */
531 public function testGetSubpageText( $title, $expected, $msg = '' ) {
532 $title = Title::newFromText( $title );
533 $this->assertEquals( $expected,
534 $title->getSubpageText(),
535 $msg
536 );
537 }
538
539 public static function provideSubpageTitleCases() {
540 return [
541 # Title, expected base, optional message
542 [ 'User:John_Doe/subOne/subTwo', 'subTwo' ],
543 [ 'User:John_Doe/subOne', 'subOne' ],
544 ];
545 }
546
547 public static function provideNewFromTitleValue() {
548 return [
549 [ new TitleValue( NS_MAIN, 'Foo' ) ],
550 [ new TitleValue( NS_MAIN, 'Foo', 'bar' ) ],
551 [ new TitleValue( NS_USER, 'Hansi_Maier' ) ],
552 ];
553 }
554
555 /**
556 * @covers Title::newFromTitleValue
557 * @dataProvider provideNewFromTitleValue
558 */
559 public function testNewFromTitleValue( TitleValue $value ) {
560 $title = Title::newFromTitleValue( $value );
561
562 $dbkey = str_replace( ' ', '_', $value->getText() );
563 $this->assertEquals( $dbkey, $title->getDBkey() );
564 $this->assertEquals( $value->getNamespace(), $title->getNamespace() );
565 $this->assertEquals( $value->getFragment(), $title->getFragment() );
566 }
567
568 /**
569 * @covers Title::newFromLinkTarget
570 * @dataProvider provideNewFromTitleValue
571 */
572 public function testNewFromLinkTarget( LinkTarget $value ) {
573 $title = Title::newFromLinkTarget( $value );
574
575 $dbkey = str_replace( ' ', '_', $value->getText() );
576 $this->assertEquals( $dbkey, $title->getDBkey() );
577 $this->assertEquals( $value->getNamespace(), $title->getNamespace() );
578 $this->assertEquals( $value->getFragment(), $title->getFragment() );
579 }
580
581 /**
582 * @covers Title::newFromLinkTarget
583 */
584 public function testNewFromLinkTarget_clone() {
585 $title = Title::newFromText( __METHOD__ );
586 $this->assertSame( $title, Title::newFromLinkTarget( $title ) );
587
588 // The Title::NEW_CLONE flag should ensure that a fresh instance is returned.
589 $clone = Title::newFromLinkTarget( $title, Title::NEW_CLONE );
590 $this->assertNotSame( $title, $clone );
591 $this->assertTrue( $clone->equals( $title ) );
592 }
593
594 public static function provideGetTitleValue() {
595 return [
596 [ 'Foo' ],
597 [ 'Foo#bar' ],
598 [ 'User:Hansi_Maier' ],
599 ];
600 }
601
602 /**
603 * @covers Title::getTitleValue
604 * @dataProvider provideGetTitleValue
605 */
606 public function testGetTitleValue( $text ) {
607 $title = Title::newFromText( $text );
608 $value = $title->getTitleValue();
609
610 $dbkey = str_replace( ' ', '_', $value->getText() );
611 $this->assertEquals( $title->getDBkey(), $dbkey );
612 $this->assertEquals( $title->getNamespace(), $value->getNamespace() );
613 $this->assertEquals( $title->getFragment(), $value->getFragment() );
614 }
615
616 public static function provideGetFragment() {
617 return [
618 [ 'Foo', '' ],
619 [ 'Foo#bar', 'bar' ],
620 [ 'Foo#bär', 'bär' ],
621
622 // Inner whitespace is normalized
623 [ 'Foo#bar_bar', 'bar bar' ],
624 [ 'Foo#bar bar', 'bar bar' ],
625 [ 'Foo#bar bar', 'bar bar' ],
626
627 // Leading whitespace is kept, trailing whitespace is trimmed.
628 // XXX: Is this really want we want?
629 [ 'Foo#_bar_bar_', ' bar bar' ],
630 [ 'Foo# bar bar ', ' bar bar' ],
631 ];
632 }
633
634 /**
635 * @covers Title::getFragment
636 * @dataProvider provideGetFragment
637 *
638 * @param string $full
639 * @param string $fragment
640 */
641 public function testGetFragment( $full, $fragment ) {
642 $title = Title::newFromText( $full );
643 $this->assertEquals( $fragment, $title->getFragment() );
644 }
645
646 /**
647 * @covers Title::isAlwaysKnown
648 * @dataProvider provideIsAlwaysKnown
649 * @param string $page
650 * @param bool $isKnown
651 */
652 public function testIsAlwaysKnown( $page, $isKnown ) {
653 $title = Title::newFromText( $page );
654 $this->assertEquals( $isKnown, $title->isAlwaysKnown() );
655 }
656
657 public static function provideIsAlwaysKnown() {
658 return [
659 [ 'Some nonexistent page', false ],
660 [ 'UTPage', false ],
661 [ '#test', true ],
662 [ 'Special:BlankPage', true ],
663 [ 'Special:SomeNonexistentSpecialPage', false ],
664 [ 'MediaWiki:Parentheses', true ],
665 [ 'MediaWiki:Some nonexistent message', false ],
666 ];
667 }
668
669 /**
670 * @covers Title::isValid
671 * @dataProvider provideIsValid
672 * @param Title $title
673 * @param bool $isValid
674 */
675 public function testIsValid( Title $title, $isValid ) {
676 $this->assertEquals( $isValid, $title->isValid(), $title->getPrefixedText() );
677 }
678
679 public static function provideIsValid() {
680 return [
681 [ Title::makeTitle( NS_MAIN, '' ), false ],
682 [ Title::makeTitle( NS_MAIN, '<>' ), false ],
683 [ Title::makeTitle( NS_MAIN, '|' ), false ],
684 [ Title::makeTitle( NS_MAIN, '#' ), false ],
685 [ Title::makeTitle( NS_MAIN, 'Test' ), true ],
686 [ Title::makeTitle( -33, 'Test' ), false ],
687 [ Title::makeTitle( 77663399, 'Test' ), false ],
688 ];
689 }
690
691 /**
692 * @covers Title::isAlwaysKnown
693 */
694 public function testIsAlwaysKnownOnInterwiki() {
695 $title = Title::makeTitle( NS_MAIN, 'Interwiki link', '', 'externalwiki' );
696 $this->assertTrue( $title->isAlwaysKnown() );
697 }
698
699 /**
700 * @covers Title::exists
701 */
702 public function testExists() {
703 $title = Title::makeTitle( NS_PROJECT, 'New page' );
704 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
705
706 $article = new Article( $title );
707 $page = $article->getPage();
708 $page->doEditContent( new WikitextContent( 'Some [[link]]' ), 'summary' );
709
710 // Tell Title it doesn't know whether it exists
711 $title->mArticleID = -1;
712
713 // Tell the link cache it doesn't exists when it really does
714 $linkCache->clearLink( $title );
715 $linkCache->addBadLinkObj( $title );
716
717 $this->assertEquals(
718 false,
719 $title->exists(),
720 'exists() should rely on link cache unless GAID_FOR_UPDATE is used'
721 );
722 $this->assertEquals(
723 true,
724 $title->exists( Title::GAID_FOR_UPDATE ),
725 'exists() should re-query database when GAID_FOR_UPDATE is used'
726 );
727 }
728
729 public function provideCanHaveTalkPage() {
730 return [
731 'User page has talk page' => [
732 Title::makeTitle( NS_USER, 'Jane' ), true
733 ],
734 'Talke page has talk page' => [
735 Title::makeTitle( NS_TALK, 'Foo' ), true
736 ],
737 'Special page cannot have talk page' => [
738 Title::makeTitle( NS_SPECIAL, 'Thing' ), false
739 ],
740 'Virtual namespace cannot have talk page' => [
741 Title::makeTitle( NS_MEDIA, 'Kitten.jpg' ), false
742 ],
743 ];
744 }
745
746 /**
747 * @dataProvider provideCanHaveTalkPage
748 * @covers Title::canHaveTalkPage
749 *
750 * @param Title $title
751 * @param bool $expected
752 */
753 public function testCanHaveTalkPage( Title $title, $expected ) {
754 $actual = $title->canHaveTalkPage();
755 $this->assertSame( $expected, $actual, $title->getPrefixedDBkey() );
756 }
757
758 public static function provideGetTalkPage_good() {
759 return [
760 [ Title::makeTitle( NS_MAIN, 'Test' ), Title::makeTitle( NS_TALK, 'Test' ) ],
761 [ Title::makeTitle( NS_TALK, 'Test' ), Title::makeTitle( NS_TALK, 'Test' ) ],
762 ];
763 }
764
765 /**
766 * @dataProvider provideGetTalkPage_good
767 * @covers Title::getTalkPage
768 */
769 public function testGetTalkPage_good( Title $title, Title $expected ) {
770 $talk = $title->getTalkPage();
771 $this->assertSame(
772 $expected->getPrefixedDBKey(),
773 $talk->getPrefixedDBKey(),
774 $title->getPrefixedDBKey()
775 );
776 }
777
778 /**
779 * @dataProvider provideGetTalkPage_good
780 * @covers Title::getTalkPageIfDefined
781 */
782 public function testGetTalkPageIfDefined_good( Title $title ) {
783 $talk = $title->getTalkPageIfDefined();
784 $this->assertInstanceOf(
785 Title::class,
786 $talk,
787 $title->getPrefixedDBKey()
788 );
789 }
790
791 public static function provideGetTalkPage_bad() {
792 return [
793 [ Title::makeTitle( NS_SPECIAL, 'Test' ) ],
794 [ Title::makeTitle( NS_MEDIA, 'Test' ) ],
795 ];
796 }
797
798 /**
799 * @dataProvider provideGetTalkPage_bad
800 * @covers Title::getTalkPageIfDefined
801 */
802 public function testGetTalkPageIfDefined_bad( Title $title ) {
803 $talk = $title->getTalkPageIfDefined();
804 $this->assertNull(
805 $talk,
806 $title->getPrefixedDBKey()
807 );
808 }
809
810 public function provideCreateFragmentTitle() {
811 return [
812 [ Title::makeTitle( NS_MAIN, 'Test' ), 'foo' ],
813 [ Title::makeTitle( NS_TALK, 'Test', 'foo' ), '' ],
814 [ Title::makeTitle( NS_CATEGORY, 'Test', 'foo' ), 'bar' ],
815 [ Title::makeTitle( NS_MAIN, 'Test1', '', 'interwiki' ), 'baz' ]
816 ];
817 }
818
819 /**
820 * @covers Title::createFragmentTarget
821 * @dataProvider provideCreateFragmentTitle
822 */
823 public function testCreateFragmentTitle( Title $title, $fragment ) {
824 $this->mergeMwGlobalArrayValue( 'wgHooks', [
825 'InterwikiLoadPrefix' => [
826 function ( $prefix, &$iwdata ) {
827 if ( $prefix === 'interwiki' ) {
828 $iwdata = [
829 'iw_url' => 'http://example.com/',
830 'iw_local' => 0,
831 'iw_trans' => 0,
832 ];
833 return false;
834 }
835 },
836 ],
837 ] );
838
839 $fragmentTitle = $title->createFragmentTarget( $fragment );
840
841 $this->assertEquals( $title->getNamespace(), $fragmentTitle->getNamespace() );
842 $this->assertEquals( $title->getText(), $fragmentTitle->getText() );
843 $this->assertEquals( $title->getInterwiki(), $fragmentTitle->getInterwiki() );
844 $this->assertEquals( $fragment, $fragmentTitle->getFragment() );
845 }
846
847 public function provideGetPrefixedText() {
848 return [
849 // ns = 0
850 [
851 Title::makeTitle( NS_MAIN, 'Foo bar' ),
852 'Foo bar'
853 ],
854 // ns = 2
855 [
856 Title::makeTitle( NS_USER, 'Foo bar' ),
857 'User:Foo bar'
858 ],
859 // ns = 3
860 [
861 Title::makeTitle( NS_USER_TALK, 'Foo bar' ),
862 'User talk:Foo bar'
863 ],
864 // fragment not included
865 [
866 Title::makeTitle( NS_MAIN, 'Foo bar', 'fragment' ),
867 'Foo bar'
868 ],
869 // ns = -2
870 [
871 Title::makeTitle( NS_MEDIA, 'Foo bar' ),
872 'Media:Foo bar'
873 ],
874 // non-existent namespace
875 [
876 Title::makeTitle( 100777, 'Foo bar' ),
877 'Special:Badtitle/NS100777:Foo bar'
878 ],
879 ];
880 }
881
882 /**
883 * @covers Title::getPrefixedText
884 * @dataProvider provideGetPrefixedText
885 */
886 public function testGetPrefixedText( Title $title, $expected ) {
887 $this->assertEquals( $expected, $title->getPrefixedText() );
888 }
889
890 public function provideGetPrefixedDBKey() {
891 return [
892 // ns = 0
893 [
894 Title::makeTitle( NS_MAIN, 'Foo_bar' ),
895 'Foo_bar'
896 ],
897 // ns = 2
898 [
899 Title::makeTitle( NS_USER, 'Foo_bar' ),
900 'User:Foo_bar'
901 ],
902 // ns = 3
903 [
904 Title::makeTitle( NS_USER_TALK, 'Foo_bar' ),
905 'User_talk:Foo_bar'
906 ],
907 // fragment not included
908 [
909 Title::makeTitle( NS_MAIN, 'Foo_bar', 'fragment' ),
910 'Foo_bar'
911 ],
912 // ns = -2
913 [
914 Title::makeTitle( NS_MEDIA, 'Foo_bar' ),
915 'Media:Foo_bar'
916 ],
917 // non-existent namespace
918 [
919 Title::makeTitle( 100777, 'Foo_bar' ),
920 'Special:Badtitle/NS100777:Foo_bar'
921 ],
922 ];
923 }
924
925 /**
926 * @covers Title::getPrefixedDBKey
927 * @dataProvider provideGetPrefixedDBKey
928 */
929 public function testGetPrefixedDBKey( Title $title, $expected ) {
930 $this->assertEquals( $expected, $title->getPrefixedDBkey() );
931 }
932
933 /**
934 * @covers Title::getFragmentForURL
935 * @dataProvider provideGetFragmentForURL
936 *
937 * @param string $titleStr
938 * @param string $expected
939 */
940 public function testGetFragmentForURL( $titleStr, $expected ) {
941 $this->setMwGlobals( [
942 'wgFragmentMode' => [ 'html5' ],
943 'wgExternalInterwikiFragmentMode' => 'legacy',
944 ] );
945 $dbw = wfGetDB( DB_MASTER );
946 $dbw->insert( 'interwiki',
947 [
948 [
949 'iw_prefix' => 'de',
950 'iw_url' => 'http://de.wikipedia.org/wiki/',
951 'iw_api' => 'http://de.wikipedia.org/w/api.php',
952 'iw_wikiid' => 'dewiki',
953 'iw_local' => 1,
954 'iw_trans' => 0,
955 ],
956 [
957 'iw_prefix' => 'zz',
958 'iw_url' => 'http://zzwiki.org/wiki/',
959 'iw_api' => 'http://zzwiki.org/w/api.php',
960 'iw_wikiid' => 'zzwiki',
961 'iw_local' => 0,
962 'iw_trans' => 0,
963 ],
964 ],
965 __METHOD__,
966 [ 'IGNORE' ]
967 );
968
969 $title = Title::newFromText( $titleStr );
970 self::assertEquals( $expected, $title->getFragmentForURL() );
971
972 $dbw->delete( 'interwiki', '*', __METHOD__ );
973 }
974
975 public function provideGetFragmentForURL() {
976 return [
977 [ 'Foo', '' ],
978 [ 'Foo#ümlåût', '#ümlåût' ],
979 [ 'de:Foo#Bå®', '#Bå®' ],
980 [ 'zz:Foo#тест', '#.D1.82.D0.B5.D1.81.D1.82' ],
981 ];
982 }
983
984 /**
985 * @covers Title::isRawHtmlMessage
986 * @dataProvider provideIsRawHtmlMessage
987 */
988 public function testIsRawHtmlMessage( $textForm, $expected ) {
989 $this->setMwGlobals( 'wgRawHtmlMessages', [
990 'foobar',
991 'foo_bar',
992 'foo-bar',
993 ] );
994
995 $title = Title::newFromText( $textForm );
996 $this->assertSame( $expected, $title->isRawHtmlMessage() );
997 }
998
999 public function provideIsRawHtmlMessage() {
1000 return [
1001 [ 'MediaWiki:Foobar', true ],
1002 [ 'MediaWiki:Foo bar', true ],
1003 [ 'MediaWiki:Foo-bar', true ],
1004 [ 'MediaWiki:foo bar', true ],
1005 [ 'MediaWiki:foo-bar', true ],
1006 [ 'MediaWiki:foobar', true ],
1007 [ 'MediaWiki:some-other-message', false ],
1008 [ 'Main Page', false ],
1009 ];
1010 }
1011
1012 public function provideEquals() {
1013 yield [
1014 Title::newFromText( 'Main Page' ),
1015 Title::newFromText( 'Main Page' ),
1016 true
1017 ];
1018 yield [
1019 Title::newFromText( 'Main Page' ),
1020 Title::newFromText( 'Not The Main Page' ),
1021 false
1022 ];
1023 yield [
1024 Title::newFromText( 'Main Page' ),
1025 Title::newFromText( 'Project:Main Page' ),
1026 false
1027 ];
1028 yield [
1029 Title::newFromText( 'File:Example.png' ),
1030 Title::newFromText( 'Image:Example.png' ),
1031 true
1032 ];
1033 yield [
1034 Title::newFromText( 'Special:Version' ),
1035 Title::newFromText( 'Special:Version' ),
1036 true
1037 ];
1038 yield [
1039 Title::newFromText( 'Special:Version' ),
1040 Title::newFromText( 'Special:Recentchanges' ),
1041 false
1042 ];
1043 yield [
1044 Title::newFromText( 'Special:Version' ),
1045 Title::newFromText( 'Main Page' ),
1046 false
1047 ];
1048 yield [
1049 Title::makeTitle( NS_MAIN, 'Foo', '', '' ),
1050 Title::makeTitle( NS_MAIN, 'Foo', '', '' ),
1051 true
1052 ];
1053 yield [
1054 Title::makeTitle( NS_MAIN, 'Foo', '', '' ),
1055 Title::makeTitle( NS_MAIN, 'Bar', '', '' ),
1056 false
1057 ];
1058 yield [
1059 Title::makeTitle( NS_MAIN, 'Foo', '', '' ),
1060 Title::makeTitle( NS_TALK, 'Foo', '', '' ),
1061 false
1062 ];
1063 yield [
1064 Title::makeTitle( NS_MAIN, 'Foo', 'Bar', '' ),
1065 Title::makeTitle( NS_MAIN, 'Foo', 'Bar', '' ),
1066 true
1067 ];
1068 yield [
1069 Title::makeTitle( NS_MAIN, 'Foo', 'Bar', '' ),
1070 Title::makeTitle( NS_MAIN, 'Foo', 'Baz', '' ),
1071 true
1072 ];
1073 yield [
1074 Title::makeTitle( NS_MAIN, 'Foo', 'Bar', '' ),
1075 Title::makeTitle( NS_MAIN, 'Foo', '', '' ),
1076 true
1077 ];
1078 yield [
1079 Title::makeTitle( NS_MAIN, 'Foo', '', 'baz' ),
1080 Title::makeTitle( NS_MAIN, 'Foo', '', 'baz' ),
1081 true
1082 ];
1083 yield [
1084 Title::makeTitle( NS_MAIN, 'Foo', '', '' ),
1085 Title::makeTitle( NS_MAIN, 'Foo', '', 'baz' ),
1086 false
1087 ];
1088 }
1089
1090 /**
1091 * @covers Title::equals
1092 * @dataProvider provideEquals
1093 */
1094 public function testEquals( Title $firstValue, /* LinkTarget */ $secondValue, $expectedSame ) {
1095 $this->assertSame(
1096 $expectedSame,
1097 $firstValue->equals( $secondValue )
1098 );
1099 }
1100 }