3 namespace MediaWiki\Tests\Api
;
5 use MediaWiki\Api\ApiMain
;
6 use MediaWiki\Api\ApiPageSet
;
7 use MediaWiki\Api\ApiResult
;
8 use MediaWiki\Context\RequestContext
;
9 use MediaWiki\Linker\LinkTarget
;
10 use MediaWiki\MainConfigNames
;
11 use MediaWiki\Page\PageIdentity
;
12 use MediaWiki\Page\PageReference
;
13 use MediaWiki\Request\FauxRequest
;
14 use MediaWiki\Tests\Unit\DummyServicesTrait
;
15 use MediaWiki\Title\Title
;
16 use MediaWiki\Title\TitleValue
;
18 use Wikimedia\TestingAccessWrapper
;
24 * @covers \MediaWiki\Api\ApiPageSet
26 class ApiPageSetTest
extends ApiTestCase
{
27 use DummyServicesTrait
;
29 public static function provideRedirectMergePolicy() {
31 'By default nothing is merged' => [
36 'A simple merge policy adds the redirect data in' => [
37 static function ( $current, $new ) {
38 if ( !isset( $current['index'] ) ||
$new['index'] < $current['index'] ) {
39 $current['index'] = $new['index'];
49 * @dataProvider provideRedirectMergePolicy
51 public function testRedirectMergePolicyWithArrayResult( $mergePolicy, $expect ) {
52 [ $target, $pageSet ] = $this->createPageSetWithRedirect();
53 $pageSet->setRedirectMergePolicy( $mergePolicy );
55 $target->getArticleID() => []
57 $pageSet->populateGeneratorData( $result );
58 $this->assertEquals( $expect, $result[$target->getArticleID()] );
62 * @dataProvider provideRedirectMergePolicy
64 public function testRedirectMergePolicyWithApiResult( $mergePolicy, $expect ) {
65 [ $target, $pageSet ] = $this->createPageSetWithRedirect();
66 $pageSet->setRedirectMergePolicy( $mergePolicy );
67 $result = new ApiResult( false );
68 $result->addValue( null, 'pages', [
69 $target->getArticleID() => []
71 $pageSet->populateGeneratorData( $result, [ 'pages' ] );
74 $result->getResultData( [ 'pages', $target->getArticleID() ] )
78 private function newApiPageSet( $reqParams = [] ) {
79 $request = new FauxRequest( $reqParams );
80 $context = new RequestContext();
81 $context->setRequest( $request );
83 $main = new ApiMain( $context );
84 $pageSet = new ApiPageSet( $main );
89 protected function createPageSetWithRedirect( $targetContent = 'api page set test' ) {
90 $target = Title
::makeTitle( NS_MAIN
, 'UTRedirectTarget' );
91 $sourceA = Title
::makeTitle( NS_MAIN
, 'UTRedirectSourceA' );
92 $sourceB = Title
::makeTitle( NS_MAIN
, 'UTRedirectSourceB' );
93 $this->editPage( 'UTRedirectTarget', $targetContent );
94 $this->editPage( 'UTRedirectSourceA', '#REDIRECT [[UTRedirectTarget]]' );
95 $this->editPage( 'UTRedirectSourceB', '#REDIRECT [[UTRedirectTarget]]' );
97 $pageSet = $this->newApiPageSet( [ 'redirects' => 1 ] );
99 $pageSet->setGeneratorData( $sourceA, [ 'index' => 1 ] );
100 $pageSet->setGeneratorData( $sourceB, [ 'index' => 3 ] );
101 $pageSet->populateFromTitles( [ $sourceA, $sourceB ] );
103 return [ $target, $pageSet ];
106 public function testRedirectMergePolicyRedirectLoop() {
107 $this->hideDeprecated( ApiPageSet
::class . '::getTitles' );
109 $redirectOneTitle = 'ApiPageSetTestRedirectOne';
110 $redirectTwoTitle = 'ApiPageSetTestRedirectTwo';
111 $this->editPage( $redirectOneTitle, "#REDIRECT [[$redirectTwoTitle]]" );
112 $this->editPage( $redirectTwoTitle, "#REDIRECT [[$redirectOneTitle]]" );
113 [ $target, $pageSet ] = $this->createPageSetWithRedirect(
114 "#REDIRECT [[$redirectOneTitle]]"
116 $pageSet->setRedirectMergePolicy( static function ( $cur, $new ) {
117 throw new RuntimeException( 'unreachable, no merge when target is redirect loop' );
119 // This could infinite loop in a bugged impl, but php doesn't offer
120 // a great way to time constrain this.
121 $result = new ApiResult( false );
122 $pageSet->populateGeneratorData( $result );
123 // Assert something, mostly we care that the above didn't infinite loop.
124 // This verifies the page set followed our redirect chain and saw the loop.
125 $this->assertEqualsCanonicalizing(
127 'UTRedirectSourceA', 'UTRedirectSourceB', 'UTRedirectTarget',
128 $redirectOneTitle, $redirectTwoTitle,
130 array_map( static function ( $x ) {
131 return $x->getPrefixedText();
132 }, $pageSet->getTitles() )
136 public function testHandleNormalization() {
137 $pageSet = $this->newApiPageSet( [ 'titles' => "a|B|a\xcc\x8a" ] );
141 [ 0 => [ 'A' => -1, 'B' => -2, 'Å' => -3 ] ],
142 $pageSet->getAllTitlesByNamespace()
146 [ 'fromencoded' => true, 'from' => 'a%CC%8A', 'to' => 'å' ],
147 [ 'fromencoded' => false, 'from' => 'a', 'to' => 'A' ],
148 [ 'fromencoded' => false, 'from' => 'å', 'to' => 'Å' ],
150 $pageSet->getNormalizedTitlesAsResult()
154 public static function provideConversionWithRedirects() {
156 'convert, redirect, convert' => [
158 'Esttay 1' => '#REDIRECT [[Test 2]]',
161 [ 'titles' => 'Test 1', 'converttitles' => 1, 'redirects' => 1 ],
163 [ 'from' => 'Test 1', 'to' => 'Esttay 1' ],
164 [ 'from' => 'Test 2', 'to' => 'Esttay 2' ]
166 [ [ 'from' => 'Esttay 1', 'to' => 'Test 2' ] ],
169 'redirect, convert, redirect' => [
171 'Esttay 1' => '#REDIRECT [[Test 2]]',
172 'Esttay 2' => '#REDIRECT [[Esttay 3]]',
174 [ 'titles' => 'Esttay 1', 'converttitles' => 1, 'redirects' => 1 ],
175 [ [ 'from' => 'Test 2', 'to' => 'Esttay 2' ] ],
177 [ 'from' => 'Esttay 1', 'to' => 'Test 2' ],
178 [ 'from' => 'Esttay 2', 'to' => 'Esttay 3' ]
182 'self-redirect to variant, with converttitles' => [
183 [ 'Esttay' => '#REDIRECT [[Test]]' ],
184 [ 'titles' => 'Esttay', 'converttitles' => 1, 'redirects' => 1 ],
185 [ [ 'from' => 'Test', 'to' => 'Esttay' ] ],
186 [ [ 'from' => 'Esttay', 'to' => 'Test' ] ],
189 'self-redirect to variant, without converttitles' => [
190 [ 'Esttay' => '#REDIRECT [[Test]]' ],
191 [ 'titles' => 'Esttay', 'redirects' => 1 ],
193 [ [ 'from' => 'Esttay', 'to' => 'Test' ] ],
199 * @dataProvider provideConversionWithRedirects
201 public function testHandleConversionWithRedirects( $pages, $params, $expectedConversion, $expectedRedirects ) {
202 $this->overrideConfigValue( MainConfigNames
::UsePigLatinVariant
, true );
203 foreach ( $pages as $title => $content ) {
204 $this->editPage( $title, $content );
207 $pageSet = $this->newApiPageSet( $params );
210 $this->assertSame( $expectedConversion, $pageSet->getConvertedTitlesAsResult() );
211 $this->assertSame( $expectedRedirects, $pageSet->getRedirectTitlesAsResult() );
214 public function testSpecialRedirects() {
215 $id1 = $this->editPage( 'UTApiPageSet', 'UTApiPageSet in the default language' )
216 ->getNewRevision()->getPageId();
217 $id2 = $this->editPage( 'UTApiPageSet/de', 'UTApiPageSet in German' )
218 ->getNewRevision()->getPageId();
220 $user = $this->getTestUser()->getUser();
221 $userName = $user->getName();
222 $userDbkey = str_replace( ' ', '_', $userName );
223 $request = new FauxRequest( [
224 'titles' => implode( '|', [
225 'Special:MyContributions',
227 'Special:MyTalk/subpage',
228 'Special:MyLanguage/UTApiPageSet',
231 $context = new RequestContext();
232 $context->setRequest( $request );
233 $context->setUser( $user );
235 $main = new ApiMain( $context );
236 $pageSet = new ApiPageSet( $main );
239 $this->assertEquals( [
240 ], $pageSet->getRedirectTitlesAsResult() );
241 $this->assertEquals( [
242 [ 'ns' => NS_SPECIAL
, 'title' => 'Special:MyContributions', 'special' => true ],
243 [ 'ns' => NS_SPECIAL
, 'title' => 'Special:MyPage', 'special' => true ],
244 [ 'ns' => NS_SPECIAL
, 'title' => 'Special:MyTalk/subpage', 'special' => true ],
245 [ 'ns' => NS_SPECIAL
, 'title' => 'Special:MyLanguage/UTApiPageSet', 'special' => true ],
246 ], $pageSet->getInvalidTitlesAndRevisions() );
247 $this->assertEquals( [
248 ], $pageSet->getAllTitlesByNamespace() );
250 $request->setVal( 'redirects', 1 );
251 $main = new ApiMain( $context );
252 $pageSet = new ApiPageSet( $main );
255 $this->assertEquals( [
256 [ 'from' => 'Special:MyPage', 'to' => "User:$userName" ],
257 [ 'from' => 'Special:MyTalk/subpage', 'to' => "User talk:$userName/subpage" ],
258 [ 'from' => 'Special:MyLanguage/UTApiPageSet', 'to' => 'UTApiPageSet' ],
259 ], $pageSet->getRedirectTitlesAsResult() );
260 $this->assertEquals( [
261 [ 'ns' => NS_SPECIAL
, 'title' => 'Special:MyContributions', 'special' => true ],
262 [ 'ns' => NS_USER
, 'title' => "User:$userName", 'missing' => true ],
263 [ 'ns' => NS_USER_TALK
, 'title' => "User talk:$userName/subpage", 'missing' => true ],
264 ], $pageSet->getInvalidTitlesAndRevisions() );
265 $this->assertEquals( [
266 NS_MAIN
=> [ 'UTApiPageSet' => $id1 ],
267 NS_USER
=> [ $userDbkey => -2 ],
268 NS_USER_TALK
=> [ "$userDbkey/subpage" => -3 ],
269 ], $pageSet->getAllTitlesByNamespace() );
271 $context->setLanguage( 'de' );
272 $main = new ApiMain( $context );
273 $pageSet = new ApiPageSet( $main );
276 $this->assertEquals( [
277 [ 'from' => 'Special:MyPage', 'to' => "User:$userName" ],
278 [ 'from' => 'Special:MyTalk/subpage', 'to' => "User talk:$userName/subpage" ],
279 [ 'from' => 'Special:MyLanguage/UTApiPageSet', 'to' => 'UTApiPageSet/de' ],
280 ], $pageSet->getRedirectTitlesAsResult() );
281 $this->assertEquals( [
282 [ 'ns' => NS_SPECIAL
, 'title' => 'Special:MyContributions', 'special' => true ],
283 [ 'ns' => NS_USER
, 'title' => "User:$userName", 'missing' => true ],
284 [ 'ns' => NS_USER_TALK
, 'title' => "User talk:$userName/subpage", 'missing' => true ],
285 ], $pageSet->getInvalidTitlesAndRevisions() );
286 $this->assertEquals( [
287 NS_MAIN
=> [ 'UTApiPageSet/de' => $id2 ],
288 NS_USER
=> [ $userDbkey => -2 ],
289 NS_USER_TALK
=> [ "$userDbkey/subpage" => -3 ],
290 ], $pageSet->getAllTitlesByNamespace() );
294 * Test that ApiPageSet is calling GenderCache for provided user names to prefill the
295 * GenderCache and avoid a performance issue when loading each users' gender on its own.
296 * The test is setting the "missLimit" to 0 on the GenderCache to trigger misses logic.
297 * When the "misses" property is no longer 0 at the end of the test,
298 * something was requested which is not part of the cache. Than the test is failing.
300 public function testGenderCaching() {
301 // Create the test user now so that the cache will be empty later
302 $this->getTestSysop()->getUser();
303 // Set up the user namespace to have gender aliases to trigger the gender cache
304 $this->overrideConfigValue(
305 MainConfigNames
::ExtraGenderNamespaces
,
306 [ NS_USER
=> [ 'male' => 'Male', 'female' => 'Female' ] ]
308 $this->overrideMwServices();
310 // User names to test with - it is not needed that the user exists in the database
311 // to trigger gender cache
318 // Prepare the gender cache for testing - this is a fresh instance due to service override
319 $genderCache = TestingAccessWrapper
::newFromObject(
320 $this->getServiceContainer()->getGenderCache()
322 $genderCache->missLimit
= 0;
324 // Do an api request to trigger ApiPageSet code
325 $this->doApiRequest( [
327 'titles' => 'User:' . implode( '|User:', $userNames ),
330 $this->assertSame( 0, $genderCache->misses
,
331 'ApiPageSet does not prefill the gender cache correctly' );
332 $this->assertEquals( $userNames, array_keys( $genderCache->cache
),
333 'ApiPageSet does not prefill all users into the gender cache' );
336 public function testPopulateFromTitles() {
337 $this->hideDeprecated( ApiPageSet
::class . '::getTitles' );
338 $this->hideDeprecated( ApiPageSet
::class . '::getGoodTitles' );
339 $this->hideDeprecated( ApiPageSet
::class . '::getMissingTitles' );
340 $this->hideDeprecated( ApiPageSet
::class . '::getGoodAndMissingTitles' );
341 $this->hideDeprecated( ApiPageSet
::class . '::getRedirectTitles' );
342 $this->hideDeprecated( ApiPageSet
::class . '::getSpecialTitles' );
344 $interwikiLookup = $this->getDummyInterwikiLookup( [ 'acme' ] );
345 $this->setService( 'InterwikiLookup', $interwikiLookup );
347 $this->getExistingTestPage( 'ApiPageSetTest_existing' )->getTitle();
348 $this->getExistingTestPage( 'ApiPageSetTest_redirect_target' )->getTitle();
349 $this->getNonexistingTestPage( 'ApiPageSetTest_missing' )->getTitle();
350 $redirectTitle = $this->getExistingTestPage( 'ApiPageSetTest_redirect' )->getTitle();
351 $this->editPage( $redirectTitle, '#REDIRECT [[ApiPageSetTest_redirect_target]]' );
354 'existing' => 'ApiPageSetTest_existing',
355 'missing' => 'ApiPageSetTest_missing',
356 'invalid' => 'ApiPageSetTest|invalid',
357 'redirect' => 'ApiPageSetTest_redirect',
358 'special' => 'Special:BlankPage',
359 'interwiki' => 'acme:ApiPageSetTest',
362 $pageSet = $this->newApiPageSet( [ 'redirects' => 1 ] );
363 $pageSet->populateFromTitles( $input );
366 new TitleValue( NS_MAIN
, 'ApiPageSetTest_existing' ),
367 new TitleValue( NS_MAIN
, 'ApiPageSetTest_redirect' ),
368 new TitleValue( NS_MAIN
, 'ApiPageSetTest_missing' ),
370 // the redirect page and the target are included!
371 new TitleValue( NS_MAIN
, 'ApiPageSetTest_redirect_target' ),
373 $this->assertLinkTargets( Title
::class, $expectedPages, $pageSet->getTitles() );
374 $this->assertLinkTargets( PageIdentity
::class, $expectedPages, $pageSet->getPages() );
377 new TitleValue( NS_MAIN
, 'ApiPageSetTest_existing' ),
378 new TitleValue( NS_MAIN
, 'ApiPageSetTest_redirect_target' )
380 $this->assertLinkTargets( Title
::class, $expectedGood, $pageSet->getGoodTitles() );
381 $this->assertLinkTargets( PageIdentity
::class, $expectedGood, $pageSet->getGoodPages() );
383 $expectedMissing = [ new TitleValue( NS_MAIN
, 'ApiPageSetTest_missing' ) ];
384 $this->assertLinkTargets(
387 $pageSet->getMissingTitles()
389 $this->assertLinkTargets(
392 $pageSet->getMissingPages()
395 [ NS_MAIN
=> [ 'ApiPageSetTest_missing' => -3 ] ],
396 $pageSet->getMissingTitlesByNamespace()
399 $expectedGoodAndMissing = array_merge( $expectedGood, $expectedMissing );
400 $this->assertLinkTargets(
402 $expectedGoodAndMissing,
403 $pageSet->getGoodAndMissingTitles()
405 $this->assertLinkTargets(
407 $expectedGoodAndMissing,
408 $pageSet->getGoodAndMissingPages()
411 $expectedSpecial = [ new TitleValue( NS_SPECIAL
, 'BlankPage' ) ];
412 $this->assertLinkTargets( Title
::class, $expectedSpecial, $pageSet->getSpecialTitles() );
413 $this->assertLinkTargets( PageReference
::class, $expectedSpecial, $pageSet->getSpecialPages() );
415 $expectedRedirects = [
416 'ApiPageSetTest redirect' => new TitleValue(
417 NS_MAIN
, 'ApiPageSetTest_redirect_target'
420 $this->assertLinkTargets( Title
::class, $expectedRedirects, $pageSet->getRedirectTitles() );
421 $this->assertLinkTargets( LinkTarget
::class, $expectedRedirects, $pageSet->getRedirectTargets() );
423 $this->assertSame( [ 'acme:ApiPageSetTest' => 'acme' ], $pageSet->getInterwikiTitles() );
425 [ [ 'title' => 'acme:ApiPageSetTest', 'iw' => 'acme' ] ],
426 $pageSet->getInterwikiTitlesAsResult()
431 'title' => 'ApiPageSetTest|invalid',
432 'invalidreason' => 'The requested page title contains invalid characters: "|".'
434 $pageSet->getInvalidTitlesAndReasons()
439 * @param string $type
440 * @param LinkTarget[] $expected
441 * @param LinkTarget[]|PageReference[] $actual
443 private function assertLinkTargets( $type, $expected, $actual ) {
445 foreach ( $expected as $expKey => $exp ) {
446 $act = current( $actual );
447 $this->assertNotFalse( $act, 'missing entry at key $expKey: ' . $exp );
449 $actKey = key( $actual );
452 if ( !is_int( $expKey ) ) {
453 $this->assertSame( $expKey, $actKey );
455 $this->assertSame( $exp->getNamespace(), $act->getNamespace() );
456 $this->assertSame( $exp->getDBkey(), $act->getDBkey() );
458 $this->assertInstanceOf( $type, $act );
460 if ( $actual instanceof LinkTarget
) {
461 $this->assertSame( $exp->getFragment(), $act->getFragment() );
462 $this->assertSame( $exp->getInterwiki(), $act->getInterwiki() );
466 $act = current( $actual );
467 $this->assertFalse( $act, 'extra entry: ' . $act );