1<?php 2 3use MediaWiki\MediaWikiServices; 4use Wikimedia\TestingAccessWrapper; 5 6/** 7 * @group API 8 * @group medium 9 * @group Database 10 * @covers ApiPageSet 11 */ 12class ApiPageSetTest extends ApiTestCase { 13 public static function provideRedirectMergePolicy() { 14 return [ 15 'By default nothing is merged' => [ 16 null, 17 [] 18 ], 19 20 'A simple merge policy adds the redirect data in' => [ 21 function ( $current, $new ) { 22 if ( !isset( $current['index'] ) || $new['index'] < $current['index'] ) { 23 $current['index'] = $new['index']; 24 } 25 return $current; 26 }, 27 [ 'index' => 1 ], 28 ], 29 ]; 30 } 31 32 /** 33 * @dataProvider provideRedirectMergePolicy 34 */ 35 public function testRedirectMergePolicyWithArrayResult( $mergePolicy, $expect ) { 36 list( $target, $pageSet ) = $this->createPageSetWithRedirect(); 37 $pageSet->setRedirectMergePolicy( $mergePolicy ); 38 $result = [ 39 $target->getArticleID() => [] 40 ]; 41 $pageSet->populateGeneratorData( $result ); 42 $this->assertEquals( $expect, $result[$target->getArticleID()] ); 43 } 44 45 /** 46 * @dataProvider provideRedirectMergePolicy 47 */ 48 public function testRedirectMergePolicyWithApiResult( $mergePolicy, $expect ) { 49 list( $target, $pageSet ) = $this->createPageSetWithRedirect(); 50 $pageSet->setRedirectMergePolicy( $mergePolicy ); 51 $result = new ApiResult( false ); 52 $result->addValue( null, 'pages', [ 53 $target->getArticleID() => [] 54 ] ); 55 $pageSet->populateGeneratorData( $result, [ 'pages' ] ); 56 $this->assertEquals( 57 $expect, 58 $result->getResultData( [ 'pages', $target->getArticleID() ] ) 59 ); 60 } 61 62 protected function createPageSetWithRedirect( $targetContent = 'api page set test' ) { 63 $target = Title::makeTitle( NS_MAIN, 'UTRedirectTarget' ); 64 $sourceA = Title::makeTitle( NS_MAIN, 'UTRedirectSourceA' ); 65 $sourceB = Title::makeTitle( NS_MAIN, 'UTRedirectSourceB' ); 66 self::editPage( 'UTRedirectTarget', $targetContent ); 67 self::editPage( 'UTRedirectSourceA', '#REDIRECT [[UTRedirectTarget]]' ); 68 self::editPage( 'UTRedirectSourceB', '#REDIRECT [[UTRedirectTarget]]' ); 69 70 $request = new FauxRequest( [ 'redirects' => 1 ] ); 71 $context = new RequestContext(); 72 $context->setRequest( $request ); 73 $main = new ApiMain( $context ); 74 $pageSet = new ApiPageSet( $main ); 75 76 $pageSet->setGeneratorData( $sourceA, [ 'index' => 1 ] ); 77 $pageSet->setGeneratorData( $sourceB, [ 'index' => 3 ] ); 78 $pageSet->populateFromTitles( [ $sourceA, $sourceB ] ); 79 80 return [ $target, $pageSet ]; 81 } 82 83 public function testRedirectMergePolicyRedirectLoop() { 84 $loopA = Title::makeTitle( NS_MAIN, 'UTPageRedirectOne' ); 85 $loopB = Title::makeTitle( NS_MAIN, 'UTPageRedirectTwo' ); 86 self::editPage( 'UTPageRedirectOne', '#REDIRECT [[UTPageRedirectTwo]]' ); 87 self::editPage( 'UTPageRedirectTwo', '#REDIRECT [[UTPageRedirectOne]]' ); 88 list( $target, $pageSet ) = $this->createPageSetWithRedirect( 89 '#REDIRECT [[UTPageRedirectOne]]' 90 ); 91 $pageSet->setRedirectMergePolicy( function ( $cur, $new ) { 92 throw new \RuntimeException( 'unreachable, no merge when target is redirect loop' ); 93 } ); 94 // This could infinite loop in a bugged impl, but php doesn't offer 95 // a great way to time constrain this. 96 $result = new ApiResult( false ); 97 $pageSet->populateGeneratorData( $result ); 98 // Assert something, mostly we care that the above didn't infinite loop. 99 // This verifies the page set followed our redirect chain and saw the loop. 100 $this->assertEqualsCanonicalizing( 101 [ 102 'UTRedirectSourceA', 'UTRedirectSourceB', 'UTRedirectTarget', 103 'UTPageRedirectOne', 'UTPageRedirectTwo', 104 ], 105 array_map( function ( $x ) { 106 return $x->getPrefixedText(); 107 }, $pageSet->getTitles() ) 108 ); 109 } 110 111 public function testHandleNormalization() { 112 $context = new RequestContext(); 113 $context->setRequest( new FauxRequest( [ 'titles' => "a|B|a\xcc\x8a" ] ) ); 114 $main = new ApiMain( $context ); 115 $pageSet = new ApiPageSet( $main ); 116 $pageSet->execute(); 117 118 $this->assertSame( 119 [ 0 => [ 'A' => -1, 'B' => -2, 'Å' => -3 ] ], 120 $pageSet->getAllTitlesByNamespace() 121 ); 122 $this->assertSame( 123 [ 124 [ 'fromencoded' => true, 'from' => 'a%CC%8A', 'to' => 'å' ], 125 [ 'fromencoded' => false, 'from' => 'a', 'to' => 'A' ], 126 [ 'fromencoded' => false, 'from' => 'å', 'to' => 'Å' ], 127 ], 128 $pageSet->getNormalizedTitlesAsResult() 129 ); 130 } 131 132 public function testSpecialRedirects() { 133 $id1 = self::editPage( 'UTApiPageSet', 'UTApiPageSet in the default language' ) 134 ->value['revision-record']->getPageId(); 135 $id2 = self::editPage( 'UTApiPageSet/de', 'UTApiPageSet in German' ) 136 ->value['revision-record']->getPageId(); 137 138 $user = $this->getTestUser()->getUser(); 139 $userName = $user->getName(); 140 $userDbkey = str_replace( ' ', '_', $userName ); 141 $request = new FauxRequest( [ 142 'titles' => implode( '|', [ 143 'Special:MyContributions', 144 'Special:MyPage', 145 'Special:MyTalk/subpage', 146 'Special:MyLanguage/UTApiPageSet', 147 ] ), 148 ] ); 149 $context = new RequestContext(); 150 $context->setRequest( $request ); 151 $context->setUser( $user ); 152 153 $main = new ApiMain( $context ); 154 $pageSet = new ApiPageSet( $main ); 155 $pageSet->execute(); 156 157 $this->assertEquals( [ 158 ], $pageSet->getRedirectTitlesAsResult() ); 159 $this->assertEquals( [ 160 [ 'ns' => -1, 'title' => 'Special:MyContributions', 'special' => true ], 161 [ 'ns' => -1, 'title' => 'Special:MyPage', 'special' => true ], 162 [ 'ns' => -1, 'title' => 'Special:MyTalk/subpage', 'special' => true ], 163 [ 'ns' => -1, 'title' => 'Special:MyLanguage/UTApiPageSet', 'special' => true ], 164 ], $pageSet->getInvalidTitlesAndRevisions() ); 165 $this->assertEquals( [ 166 ], $pageSet->getAllTitlesByNamespace() ); 167 168 $request->setVal( 'redirects', 1 ); 169 $main = new ApiMain( $context ); 170 $pageSet = new ApiPageSet( $main ); 171 $pageSet->execute(); 172 173 $this->assertEquals( [ 174 [ 'from' => 'Special:MyPage', 'to' => "User:$userName" ], 175 [ 'from' => 'Special:MyTalk/subpage', 'to' => "User talk:$userName/subpage" ], 176 [ 'from' => 'Special:MyLanguage/UTApiPageSet', 'to' => 'UTApiPageSet' ], 177 ], $pageSet->getRedirectTitlesAsResult() ); 178 $this->assertEquals( [ 179 [ 'ns' => -1, 'title' => 'Special:MyContributions', 'special' => true ], 180 [ 'ns' => 2, 'title' => "User:$userName", 'missing' => true ], 181 [ 'ns' => 3, 'title' => "User talk:$userName/subpage", 'missing' => true ], 182 ], $pageSet->getInvalidTitlesAndRevisions() ); 183 $this->assertEquals( [ 184 0 => [ 'UTApiPageSet' => $id1 ], 185 2 => [ $userDbkey => -2 ], 186 3 => [ "$userDbkey/subpage" => -3 ], 187 ], $pageSet->getAllTitlesByNamespace() ); 188 189 $context->setLanguage( 'de' ); 190 $main = new ApiMain( $context ); 191 $pageSet = new ApiPageSet( $main ); 192 $pageSet->execute(); 193 194 $this->assertEquals( [ 195 [ 'from' => 'Special:MyPage', 'to' => "User:$userName" ], 196 [ 'from' => 'Special:MyTalk/subpage', 'to' => "User talk:$userName/subpage" ], 197 [ 'from' => 'Special:MyLanguage/UTApiPageSet', 'to' => 'UTApiPageSet/de' ], 198 ], $pageSet->getRedirectTitlesAsResult() ); 199 $this->assertEquals( [ 200 [ 'ns' => -1, 'title' => 'Special:MyContributions', 'special' => true ], 201 [ 'ns' => 2, 'title' => "User:$userName", 'missing' => true ], 202 [ 'ns' => 3, 'title' => "User talk:$userName/subpage", 'missing' => true ], 203 ], $pageSet->getInvalidTitlesAndRevisions() ); 204 $this->assertEquals( [ 205 0 => [ 'UTApiPageSet/de' => $id2 ], 206 2 => [ $userDbkey => -2 ], 207 3 => [ "$userDbkey/subpage" => -3 ], 208 ], $pageSet->getAllTitlesByNamespace() ); 209 } 210 211 /** 212 * Test that ApiPageSet is calling GenderCache for provided user names to prefill the 213 * GenderCache and avoid a performance issue when loading each users' gender on it's own. 214 * The test is setting the "missLimit" to 0 on the GenderCache to trigger misses logic. 215 * When the "misses" property is no longer 0 at the end of the test, 216 * something was requested which is not part of the cache. Than the test is failing. 217 */ 218 public function testGenderCaching() { 219 // Set up the user namespace to have gender aliases to trigger the gender cache 220 $this->setMwGlobals( [ 221 'wgExtraGenderNamespaces' => [ NS_USER => [ 'male' => 'Male', 'female' => 'Female' ] ] 222 ] ); 223 $this->overrideMwServices(); 224 225 // User names to test with - it is not needed that the user exists in the database 226 // to trigger gender cache 227 $userNames = [ 228 'Female', 229 'Unknown', 230 'Male', 231 ]; 232 233 // Prepare the gender cache for testing - this is a fresh instance due to service override 234 $genderCache = TestingAccessWrapper::newFromObject( 235 MediaWikiServices::getInstance()->getGenderCache() 236 ); 237 $genderCache->missLimit = 0; 238 239 // Do an api request to trigger ApiPageSet code 240 $this->doApiRequest( [ 241 'action' => 'query', 242 'titles' => 'User:' . implode( '|User:', $userNames ), 243 ] ); 244 245 $this->assertSame( 0, $genderCache->misses, 246 'ApiPageSet does not prefill the gender cache correctly' ); 247 $this->assertEquals( $userNames, array_keys( $genderCache->cache ), 248 'ApiPageSet does not prefill all users into the gender cache' ); 249 } 250} 251