MediaWiki  master
MediaWikiTitleCodecTest.php
Go to the documentation of this file.
1 <?php
23 
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 
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 
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 
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(
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 
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 
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 
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 
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 
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 
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 
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 
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 }
A codec for MediaWiki page titles.
testParseTitle( $text, $ns, $lang, $title=null)
provideParseTitle
testGetPrefixedText( $namespace, $dbkey, $fragment, $lang, $expected)
provideGetPrefixedText
const NS_MAIN
Definition: Defines.php:60
getNamespaceInfo()
Returns a mock NamespaceInfo that has only a hasGenderDistinction() method, which assumes only NS_USE...
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
if(!isset( $args[0])) $lang
getGenderCache()
Returns a mock GenderCache that will consider a user "female" if the first part of the user name ends...
testGetFullText( $namespace, $dbkey, $fragment, $lang, $expected)
provideGetFullText
const NS_CATEGORY
Definition: Defines.php:74
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
testGetPrefixedDBkey( $namespace, $dbkey, $fragment, $interwiki, $lang, $expected)
provideGetPrefixedDBkey
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:925
static factory( $code)
Get a cached or new language object for a given language code.
Definition: Language.php:216
const NS_FILE
Definition: Defines.php:66
const NS_FILE_TALK
Definition: Defines.php:67
Service interface for looking up Interwiki records.
setMwGlobals( $pairs, $value=null)
Sets a global, maintaining a stashed version of the previous global to be restored in tearDown...
testParseTitle_invalid( $text)
provideParseTitle_invalid
getInterwikiLookup()
Returns a mock InterwikiLookup that only has an isValidInterwiki() method, which recognizes &#39;localtes...
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
testGetNamespaceName( $namespace, $text, $lang, $expected)
provideGetNamespaceName
testFormat( $namespace, $text, $fragment, $interwiki, $lang, $expected, $normalized=null)
provideFormat
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
const NS_TALK
Definition: Defines.php:61
const NS_USER_TALK
Definition: Defines.php:63
testGetText( $namespace, $dbkey, $fragment, $lang, $expected)
provideGetText