ApiPageSet.php 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520
  1. <?php
  2. /**
  3. * Copyright © 2006, 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. */
  22. use MediaWiki\MediaWikiServices;
  23. use Wikimedia\Rdbms\IResultWrapper;
  24. use Wikimedia\Rdbms\IDatabase;
  25. /**
  26. * This class contains a list of pages that the client has requested.
  27. * Initially, when the client passes in titles=, pageids=, or revisions=
  28. * parameter, an instance of the ApiPageSet class will normalize titles,
  29. * determine if the pages/revisions exist, and prefetch any additional page
  30. * data requested.
  31. *
  32. * When a generator is used, the result of the generator will become the input
  33. * for the second instance of this class, and all subsequent actions will use
  34. * the second instance for all their work.
  35. *
  36. * @ingroup API
  37. * @since 1.21 derives from ApiBase instead of ApiQueryBase
  38. */
  39. class ApiPageSet extends ApiBase {
  40. /**
  41. * Constructor flag: The new instance of ApiPageSet will ignore the 'generator=' parameter
  42. * @since 1.21
  43. */
  44. const DISABLE_GENERATORS = 1;
  45. private $mDbSource;
  46. private $mParams;
  47. private $mResolveRedirects;
  48. private $mConvertTitles;
  49. private $mAllowGenerator;
  50. private $mAllPages = []; // [ns][dbkey] => page_id or negative when missing
  51. private $mTitles = [];
  52. private $mGoodAndMissingPages = []; // [ns][dbkey] => page_id or negative when missing
  53. private $mGoodPages = []; // [ns][dbkey] => page_id
  54. private $mGoodTitles = [];
  55. private $mMissingPages = []; // [ns][dbkey] => fake page_id
  56. private $mMissingTitles = [];
  57. /** @var array [fake_page_id] => [ 'title' => $title, 'invalidreason' => $reason ] */
  58. private $mInvalidTitles = [];
  59. private $mMissingPageIDs = [];
  60. private $mRedirectTitles = [];
  61. private $mSpecialTitles = [];
  62. private $mAllSpecials = []; // separate from mAllPages to avoid breaking getAllTitlesByNamespace()
  63. private $mNormalizedTitles = [];
  64. private $mInterwikiTitles = [];
  65. /** @var Title[] */
  66. private $mPendingRedirectIDs = [];
  67. private $mPendingRedirectSpecialPages = []; // [dbkey] => [ Title $from, Title $to ]
  68. private $mResolvedRedirectTitles = [];
  69. private $mConvertedTitles = [];
  70. private $mGoodRevIDs = [];
  71. private $mLiveRevIDs = [];
  72. private $mDeletedRevIDs = [];
  73. private $mMissingRevIDs = [];
  74. private $mGeneratorData = []; // [ns][dbkey] => data array
  75. private $mFakePageId = -1;
  76. private $mCacheMode = 'public';
  77. /** @var array */
  78. private $mRequestedPageFields = [];
  79. /** @var int */
  80. private $mDefaultNamespace = NS_MAIN;
  81. /** @var callable|null */
  82. private $mRedirectMergePolicy;
  83. /**
  84. * Add all items from $values into the result
  85. * @param array $result Output
  86. * @param array $values Values to add
  87. * @param string[] $flags The names of boolean flags to mark this element
  88. * @param string $name If given, name of the value
  89. */
  90. private static function addValues( array &$result, $values, $flags = [], $name = null ) {
  91. foreach ( $values as $val ) {
  92. if ( $val instanceof Title ) {
  93. $v = [];
  94. ApiQueryBase::addTitleInfo( $v, $val );
  95. } elseif ( $name !== null ) {
  96. $v = [ $name => $val ];
  97. } else {
  98. $v = $val;
  99. }
  100. foreach ( $flags as $flag ) {
  101. $v[$flag] = true;
  102. }
  103. $result[] = $v;
  104. }
  105. }
  106. /**
  107. * @param ApiBase $dbSource Module implementing getDB().
  108. * Allows PageSet to reuse existing db connection from the shared state like ApiQuery.
  109. * @param int $flags Zero or more flags like DISABLE_GENERATORS
  110. * @param int $defaultNamespace The namespace to use if none is specified by a prefix.
  111. * @since 1.21 accepts $flags instead of two boolean values
  112. */
  113. public function __construct( ApiBase $dbSource, $flags = 0, $defaultNamespace = NS_MAIN ) {
  114. parent::__construct( $dbSource->getMain(), $dbSource->getModuleName() );
  115. $this->mDbSource = $dbSource;
  116. $this->mAllowGenerator = ( $flags & self::DISABLE_GENERATORS ) == 0;
  117. $this->mDefaultNamespace = $defaultNamespace;
  118. $this->mParams = $this->extractRequestParams();
  119. $this->mResolveRedirects = $this->mParams['redirects'];
  120. $this->mConvertTitles = $this->mParams['converttitles'];
  121. }
  122. /**
  123. * In case execute() is not called, call this method to mark all relevant parameters as used
  124. * This prevents unused parameters from being reported as warnings
  125. */
  126. public function executeDryRun() {
  127. $this->executeInternal( true );
  128. }
  129. /**
  130. * Populate the PageSet from the request parameters.
  131. */
  132. public function execute() {
  133. $this->executeInternal( false );
  134. }
  135. /**
  136. * Populate the PageSet from the request parameters.
  137. * @param bool $isDryRun If true, instantiates generator, but only to mark
  138. * relevant parameters as used
  139. */
  140. private function executeInternal( $isDryRun ) {
  141. $generatorName = $this->mAllowGenerator ? $this->mParams['generator'] : null;
  142. if ( isset( $generatorName ) ) {
  143. $dbSource = $this->mDbSource;
  144. if ( !$dbSource instanceof ApiQuery ) {
  145. // If the parent container of this pageset is not ApiQuery, we must create it to run generator
  146. $dbSource = $this->getMain()->getModuleManager()->getModule( 'query' );
  147. }
  148. $generator = $dbSource->getModuleManager()->getModule( $generatorName, null, true );
  149. if ( $generator === null ) {
  150. $this->dieWithError( [ 'apierror-badgenerator-unknown', $generatorName ], 'badgenerator' );
  151. }
  152. if ( !$generator instanceof ApiQueryGeneratorBase ) {
  153. $this->dieWithError( [ 'apierror-badgenerator-notgenerator', $generatorName ], 'badgenerator' );
  154. }
  155. // Create a temporary pageset to store generator's output,
  156. // add any additional fields generator may need, and execute pageset to populate titles/pageids
  157. $tmpPageSet = new ApiPageSet( $dbSource, self::DISABLE_GENERATORS );
  158. $generator->setGeneratorMode( $tmpPageSet );
  159. $this->mCacheMode = $generator->getCacheMode( $generator->extractRequestParams() );
  160. if ( !$isDryRun ) {
  161. $generator->requestExtraData( $tmpPageSet );
  162. }
  163. $tmpPageSet->executeInternal( $isDryRun );
  164. // populate this pageset with the generator output
  165. if ( !$isDryRun ) {
  166. $generator->executeGenerator( $this );
  167. // Avoid PHP 7.1 warning of passing $this by reference
  168. $apiModule = $this;
  169. Hooks::run( 'APIQueryGeneratorAfterExecute', [ &$generator, &$apiModule ] );
  170. } else {
  171. // Prevent warnings from being reported on these parameters
  172. $main = $this->getMain();
  173. foreach ( $generator->extractRequestParams() as $paramName => $param ) {
  174. $main->markParamsUsed( $generator->encodeParamName( $paramName ) );
  175. }
  176. }
  177. if ( !$isDryRun ) {
  178. $this->resolvePendingRedirects();
  179. }
  180. } else {
  181. // Only one of the titles/pageids/revids is allowed at the same time
  182. $dataSource = null;
  183. if ( isset( $this->mParams['titles'] ) ) {
  184. $dataSource = 'titles';
  185. }
  186. if ( isset( $this->mParams['pageids'] ) ) {
  187. if ( isset( $dataSource ) ) {
  188. $this->dieWithError(
  189. [
  190. 'apierror-invalidparammix-cannotusewith',
  191. $this->encodeParamName( 'pageids' ),
  192. $this->encodeParamName( $dataSource )
  193. ],
  194. 'multisource'
  195. );
  196. }
  197. $dataSource = 'pageids';
  198. }
  199. if ( isset( $this->mParams['revids'] ) ) {
  200. if ( isset( $dataSource ) ) {
  201. $this->dieWithError(
  202. [
  203. 'apierror-invalidparammix-cannotusewith',
  204. $this->encodeParamName( 'revids' ),
  205. $this->encodeParamName( $dataSource )
  206. ],
  207. 'multisource'
  208. );
  209. }
  210. $dataSource = 'revids';
  211. }
  212. if ( !$isDryRun ) {
  213. // Populate page information with the original user input
  214. switch ( $dataSource ) {
  215. case 'titles':
  216. $this->initFromTitles( $this->mParams['titles'] );
  217. break;
  218. case 'pageids':
  219. $this->initFromPageIds( $this->mParams['pageids'] );
  220. break;
  221. case 'revids':
  222. if ( $this->mResolveRedirects ) {
  223. $this->addWarning( 'apiwarn-redirectsandrevids' );
  224. }
  225. $this->mResolveRedirects = false;
  226. $this->initFromRevIDs( $this->mParams['revids'] );
  227. break;
  228. default:
  229. // Do nothing - some queries do not need any of the data sources.
  230. break;
  231. }
  232. }
  233. }
  234. }
  235. /**
  236. * Check whether this PageSet is resolving redirects
  237. * @return bool
  238. */
  239. public function isResolvingRedirects() {
  240. return $this->mResolveRedirects;
  241. }
  242. /**
  243. * Return the parameter name that is the source of data for this PageSet
  244. *
  245. * If multiple source parameters are specified (e.g. titles and pageids),
  246. * one will be named arbitrarily.
  247. *
  248. * @return string|null
  249. */
  250. public function getDataSource() {
  251. if ( $this->mAllowGenerator && isset( $this->mParams['generator'] ) ) {
  252. return 'generator';
  253. }
  254. if ( isset( $this->mParams['titles'] ) ) {
  255. return 'titles';
  256. }
  257. if ( isset( $this->mParams['pageids'] ) ) {
  258. return 'pageids';
  259. }
  260. if ( isset( $this->mParams['revids'] ) ) {
  261. return 'revids';
  262. }
  263. return null;
  264. }
  265. /**
  266. * Request an additional field from the page table.
  267. * Must be called before execute()
  268. * @param string $fieldName Field name
  269. */
  270. public function requestField( $fieldName ) {
  271. $this->mRequestedPageFields[$fieldName] = null;
  272. }
  273. /**
  274. * Get the value of a custom field previously requested through
  275. * requestField()
  276. * @param string $fieldName Field name
  277. * @return mixed Field value
  278. */
  279. public function getCustomField( $fieldName ) {
  280. return $this->mRequestedPageFields[$fieldName];
  281. }
  282. /**
  283. * Get the fields that have to be queried from the page table:
  284. * the ones requested through requestField() and a few basic ones
  285. * we always need
  286. * @return array Array of field names
  287. */
  288. public function getPageTableFields() {
  289. // Ensure we get minimum required fields
  290. // DON'T change this order
  291. $pageFlds = [
  292. 'page_namespace' => null,
  293. 'page_title' => null,
  294. 'page_id' => null,
  295. ];
  296. if ( $this->mResolveRedirects ) {
  297. $pageFlds['page_is_redirect'] = null;
  298. }
  299. if ( $this->getConfig()->get( 'ContentHandlerUseDB' ) ) {
  300. $pageFlds['page_content_model'] = null;
  301. }
  302. if ( $this->getConfig()->get( 'PageLanguageUseDB' ) ) {
  303. $pageFlds['page_lang'] = null;
  304. }
  305. foreach ( LinkCache::getSelectFields() as $field ) {
  306. $pageFlds[$field] = null;
  307. }
  308. $pageFlds = array_merge( $pageFlds, $this->mRequestedPageFields );
  309. return array_keys( $pageFlds );
  310. }
  311. /**
  312. * Returns an array [ns][dbkey] => page_id for all requested titles.
  313. * page_id is a unique negative number in case title was not found.
  314. * Invalid titles will also have negative page IDs and will be in namespace 0
  315. * @return array
  316. */
  317. public function getAllTitlesByNamespace() {
  318. return $this->mAllPages;
  319. }
  320. /**
  321. * All Title objects provided.
  322. * @return Title[]
  323. */
  324. public function getTitles() {
  325. return $this->mTitles;
  326. }
  327. /**
  328. * Returns the number of unique pages (not revisions) in the set.
  329. * @return int
  330. */
  331. public function getTitleCount() {
  332. return count( $this->mTitles );
  333. }
  334. /**
  335. * Returns an array [ns][dbkey] => page_id for all good titles.
  336. * @return array
  337. */
  338. public function getGoodTitlesByNamespace() {
  339. return $this->mGoodPages;
  340. }
  341. /**
  342. * Title objects that were found in the database.
  343. * @return Title[] Array page_id (int) => Title (obj)
  344. */
  345. public function getGoodTitles() {
  346. return $this->mGoodTitles;
  347. }
  348. /**
  349. * Returns the number of found unique pages (not revisions) in the set.
  350. * @return int
  351. */
  352. public function getGoodTitleCount() {
  353. return count( $this->mGoodTitles );
  354. }
  355. /**
  356. * Returns an array [ns][dbkey] => fake_page_id for all missing titles.
  357. * fake_page_id is a unique negative number.
  358. * @return array
  359. */
  360. public function getMissingTitlesByNamespace() {
  361. return $this->mMissingPages;
  362. }
  363. /**
  364. * Title objects that were NOT found in the database.
  365. * The array's index will be negative for each item
  366. * @return Title[]
  367. */
  368. public function getMissingTitles() {
  369. return $this->mMissingTitles;
  370. }
  371. /**
  372. * Returns an array [ns][dbkey] => page_id for all good and missing titles.
  373. * @return array
  374. */
  375. public function getGoodAndMissingTitlesByNamespace() {
  376. return $this->mGoodAndMissingPages;
  377. }
  378. /**
  379. * Title objects for good and missing titles.
  380. * @return array
  381. */
  382. public function getGoodAndMissingTitles() {
  383. return $this->mGoodTitles + $this->mMissingTitles;
  384. }
  385. /**
  386. * Titles that were deemed invalid by Title::newFromText()
  387. * The array's index will be unique and negative for each item
  388. * @return array[] Array of arrays with 'title' and 'invalidreason' properties
  389. */
  390. public function getInvalidTitlesAndReasons() {
  391. return $this->mInvalidTitles;
  392. }
  393. /**
  394. * Page IDs that were not found in the database
  395. * @return array Array of page IDs
  396. */
  397. public function getMissingPageIDs() {
  398. return $this->mMissingPageIDs;
  399. }
  400. /**
  401. * Get a list of redirect resolutions - maps a title to its redirect
  402. * target, as an array of output-ready arrays
  403. * @return Title[]
  404. */
  405. public function getRedirectTitles() {
  406. return $this->mRedirectTitles;
  407. }
  408. /**
  409. * Get a list of redirect resolutions - maps a title to its redirect
  410. * target. Includes generator data for redirect source when available.
  411. * @param ApiResult|null $result
  412. * @return array Array of prefixed_title (string) => Title object
  413. * @since 1.21
  414. */
  415. public function getRedirectTitlesAsResult( $result = null ) {
  416. $values = [];
  417. foreach ( $this->getRedirectTitles() as $titleStrFrom => $titleTo ) {
  418. $r = [
  419. 'from' => strval( $titleStrFrom ),
  420. 'to' => $titleTo->getPrefixedText(),
  421. ];
  422. if ( $titleTo->hasFragment() ) {
  423. $r['tofragment'] = $titleTo->getFragment();
  424. }
  425. if ( $titleTo->isExternal() ) {
  426. $r['tointerwiki'] = $titleTo->getInterwiki();
  427. }
  428. if ( isset( $this->mResolvedRedirectTitles[$titleStrFrom] ) ) {
  429. $titleFrom = $this->mResolvedRedirectTitles[$titleStrFrom];
  430. $ns = $titleFrom->getNamespace();
  431. $dbkey = $titleFrom->getDBkey();
  432. if ( isset( $this->mGeneratorData[$ns][$dbkey] ) ) {
  433. $r = array_merge( $this->mGeneratorData[$ns][$dbkey], $r );
  434. }
  435. }
  436. $values[] = $r;
  437. }
  438. if ( !empty( $values ) && $result ) {
  439. ApiResult::setIndexedTagName( $values, 'r' );
  440. }
  441. return $values;
  442. }
  443. /**
  444. * Get a list of title normalizations - maps a title to its normalized
  445. * version.
  446. * @return array Array of raw_prefixed_title (string) => prefixed_title (string)
  447. */
  448. public function getNormalizedTitles() {
  449. return $this->mNormalizedTitles;
  450. }
  451. /**
  452. * Get a list of title normalizations - maps a title to its normalized
  453. * version in the form of result array.
  454. * @param ApiResult|null $result
  455. * @return array Array of raw_prefixed_title (string) => prefixed_title (string)
  456. * @since 1.21
  457. */
  458. public function getNormalizedTitlesAsResult( $result = null ) {
  459. $values = [];
  460. $contLang = MediaWikiServices::getInstance()->getContentLanguage();
  461. foreach ( $this->getNormalizedTitles() as $rawTitleStr => $titleStr ) {
  462. $encode = $contLang->normalize( $rawTitleStr ) !== $rawTitleStr;
  463. $values[] = [
  464. 'fromencoded' => $encode,
  465. 'from' => $encode ? rawurlencode( $rawTitleStr ) : $rawTitleStr,
  466. 'to' => $titleStr
  467. ];
  468. }
  469. if ( !empty( $values ) && $result ) {
  470. ApiResult::setIndexedTagName( $values, 'n' );
  471. }
  472. return $values;
  473. }
  474. /**
  475. * Get a list of title conversions - maps a title to its converted
  476. * version.
  477. * @return array Array of raw_prefixed_title (string) => prefixed_title (string)
  478. */
  479. public function getConvertedTitles() {
  480. return $this->mConvertedTitles;
  481. }
  482. /**
  483. * Get a list of title conversions - maps a title to its converted
  484. * version as a result array.
  485. * @param ApiResult|null $result
  486. * @return array Array of (from, to) strings
  487. * @since 1.21
  488. */
  489. public function getConvertedTitlesAsResult( $result = null ) {
  490. $values = [];
  491. foreach ( $this->getConvertedTitles() as $rawTitleStr => $titleStr ) {
  492. $values[] = [
  493. 'from' => $rawTitleStr,
  494. 'to' => $titleStr
  495. ];
  496. }
  497. if ( !empty( $values ) && $result ) {
  498. ApiResult::setIndexedTagName( $values, 'c' );
  499. }
  500. return $values;
  501. }
  502. /**
  503. * Get a list of interwiki titles - maps a title to its interwiki
  504. * prefix.
  505. * @return array Array of raw_prefixed_title (string) => interwiki_prefix (string)
  506. */
  507. public function getInterwikiTitles() {
  508. return $this->mInterwikiTitles;
  509. }
  510. /**
  511. * Get a list of interwiki titles - maps a title to its interwiki
  512. * prefix as result.
  513. * @param ApiResult|null $result
  514. * @param bool $iwUrl
  515. * @return array Array of raw_prefixed_title (string) => interwiki_prefix (string)
  516. * @since 1.21
  517. */
  518. public function getInterwikiTitlesAsResult( $result = null, $iwUrl = false ) {
  519. $values = [];
  520. foreach ( $this->getInterwikiTitles() as $rawTitleStr => $interwikiStr ) {
  521. $item = [
  522. 'title' => $rawTitleStr,
  523. 'iw' => $interwikiStr,
  524. ];
  525. if ( $iwUrl ) {
  526. $title = Title::newFromText( $rawTitleStr );
  527. $item['url'] = $title->getFullURL( '', false, PROTO_CURRENT );
  528. }
  529. $values[] = $item;
  530. }
  531. if ( !empty( $values ) && $result ) {
  532. ApiResult::setIndexedTagName( $values, 'i' );
  533. }
  534. return $values;
  535. }
  536. /**
  537. * Get an array of invalid/special/missing titles.
  538. *
  539. * @param array $invalidChecks List of types of invalid titles to include.
  540. * Recognized values are:
  541. * - invalidTitles: Titles and reasons from $this->getInvalidTitlesAndReasons()
  542. * - special: Titles from $this->getSpecialTitles()
  543. * - missingIds: ids from $this->getMissingPageIDs()
  544. * - missingRevIds: ids from $this->getMissingRevisionIDs()
  545. * - missingTitles: Titles from $this->getMissingTitles()
  546. * - interwikiTitles: Titles from $this->getInterwikiTitlesAsResult()
  547. * @return array Array suitable for inclusion in the response
  548. * @since 1.23
  549. */
  550. public function getInvalidTitlesAndRevisions( $invalidChecks = [ 'invalidTitles',
  551. 'special', 'missingIds', 'missingRevIds', 'missingTitles', 'interwikiTitles' ]
  552. ) {
  553. $result = [];
  554. if ( in_array( 'invalidTitles', $invalidChecks ) ) {
  555. self::addValues( $result, $this->getInvalidTitlesAndReasons(), [ 'invalid' ] );
  556. }
  557. if ( in_array( 'special', $invalidChecks ) ) {
  558. $known = [];
  559. $unknown = [];
  560. foreach ( $this->getSpecialTitles() as $title ) {
  561. if ( $title->isKnown() ) {
  562. $known[] = $title;
  563. } else {
  564. $unknown[] = $title;
  565. }
  566. }
  567. self::addValues( $result, $unknown, [ 'special', 'missing' ] );
  568. self::addValues( $result, $known, [ 'special' ] );
  569. }
  570. if ( in_array( 'missingIds', $invalidChecks ) ) {
  571. self::addValues( $result, $this->getMissingPageIDs(), [ 'missing' ], 'pageid' );
  572. }
  573. if ( in_array( 'missingRevIds', $invalidChecks ) ) {
  574. self::addValues( $result, $this->getMissingRevisionIDs(), [ 'missing' ], 'revid' );
  575. }
  576. if ( in_array( 'missingTitles', $invalidChecks ) ) {
  577. $known = [];
  578. $unknown = [];
  579. foreach ( $this->getMissingTitles() as $title ) {
  580. if ( $title->isKnown() ) {
  581. $known[] = $title;
  582. } else {
  583. $unknown[] = $title;
  584. }
  585. }
  586. self::addValues( $result, $unknown, [ 'missing' ] );
  587. self::addValues( $result, $known, [ 'missing', 'known' ] );
  588. }
  589. if ( in_array( 'interwikiTitles', $invalidChecks ) ) {
  590. self::addValues( $result, $this->getInterwikiTitlesAsResult() );
  591. }
  592. return $result;
  593. }
  594. /**
  595. * Get the list of valid revision IDs (requested with the revids= parameter)
  596. * @return array Array of revID (int) => pageID (int)
  597. */
  598. public function getRevisionIDs() {
  599. return $this->mGoodRevIDs;
  600. }
  601. /**
  602. * Get the list of non-deleted revision IDs (requested with the revids= parameter)
  603. * @return array Array of revID (int) => pageID (int)
  604. */
  605. public function getLiveRevisionIDs() {
  606. return $this->mLiveRevIDs;
  607. }
  608. /**
  609. * Get the list of revision IDs that were associated with deleted titles.
  610. * @return array Array of revID (int) => pageID (int)
  611. */
  612. public function getDeletedRevisionIDs() {
  613. return $this->mDeletedRevIDs;
  614. }
  615. /**
  616. * Revision IDs that were not found in the database
  617. * @return array Array of revision IDs
  618. */
  619. public function getMissingRevisionIDs() {
  620. return $this->mMissingRevIDs;
  621. }
  622. /**
  623. * Revision IDs that were not found in the database as result array.
  624. * @param ApiResult|null $result
  625. * @return array Array of revision IDs
  626. * @since 1.21
  627. */
  628. public function getMissingRevisionIDsAsResult( $result = null ) {
  629. $values = [];
  630. foreach ( $this->getMissingRevisionIDs() as $revid ) {
  631. $values[$revid] = [
  632. 'revid' => $revid
  633. ];
  634. }
  635. if ( !empty( $values ) && $result ) {
  636. ApiResult::setIndexedTagName( $values, 'rev' );
  637. }
  638. return $values;
  639. }
  640. /**
  641. * Get the list of titles with negative namespace
  642. * @return Title[]
  643. */
  644. public function getSpecialTitles() {
  645. return $this->mSpecialTitles;
  646. }
  647. /**
  648. * Returns the number of revisions (requested with revids= parameter).
  649. * @return int Number of revisions.
  650. */
  651. public function getRevisionCount() {
  652. return count( $this->getRevisionIDs() );
  653. }
  654. /**
  655. * Populate this PageSet from a list of Titles
  656. * @param array $titles Array of Title objects
  657. */
  658. public function populateFromTitles( $titles ) {
  659. $this->initFromTitles( $titles );
  660. }
  661. /**
  662. * Populate this PageSet from a list of page IDs
  663. * @param array $pageIDs Array of page IDs
  664. */
  665. public function populateFromPageIDs( $pageIDs ) {
  666. $this->initFromPageIds( $pageIDs );
  667. }
  668. /**
  669. * Populate this PageSet from a rowset returned from the database
  670. *
  671. * Note that the query result must include the columns returned by
  672. * $this->getPageTableFields().
  673. *
  674. * @param IDatabase $db
  675. * @param IResultWrapper $queryResult
  676. */
  677. public function populateFromQueryResult( $db, $queryResult ) {
  678. $this->initFromQueryResult( $queryResult );
  679. }
  680. /**
  681. * Populate this PageSet from a list of revision IDs
  682. * @param array $revIDs Array of revision IDs
  683. */
  684. public function populateFromRevisionIDs( $revIDs ) {
  685. $this->initFromRevIDs( $revIDs );
  686. }
  687. /**
  688. * Extract all requested fields from the row received from the database
  689. * @param stdClass $row Result row
  690. */
  691. public function processDbRow( $row ) {
  692. // Store Title object in various data structures
  693. $title = Title::newFromRow( $row );
  694. $linkCache = MediaWikiServices::getInstance()->getLinkCache();
  695. $linkCache->addGoodLinkObjFromRow( $title, $row );
  696. $pageId = (int)$row->page_id;
  697. $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId;
  698. $this->mTitles[] = $title;
  699. if ( $this->mResolveRedirects && $row->page_is_redirect == '1' ) {
  700. $this->mPendingRedirectIDs[$pageId] = $title;
  701. } else {
  702. $this->mGoodPages[$row->page_namespace][$row->page_title] = $pageId;
  703. $this->mGoodAndMissingPages[$row->page_namespace][$row->page_title] = $pageId;
  704. $this->mGoodTitles[$pageId] = $title;
  705. }
  706. foreach ( $this->mRequestedPageFields as $fieldName => &$fieldValues ) {
  707. $fieldValues[$pageId] = $row->$fieldName;
  708. }
  709. }
  710. /**
  711. * This method populates internal variables with page information
  712. * based on the given array of title strings.
  713. *
  714. * Steps:
  715. * #1 For each title, get data from `page` table
  716. * #2 If page was not found in the DB, store it as missing
  717. *
  718. * Additionally, when resolving redirects:
  719. * #3 If no more redirects left, stop.
  720. * #4 For each redirect, get its target from the `redirect` table.
  721. * #5 Substitute the original LinkBatch object with the new list
  722. * #6 Repeat from step #1
  723. *
  724. * @param array $titles Array of Title objects or strings
  725. */
  726. private function initFromTitles( $titles ) {
  727. // Get validated and normalized title objects
  728. $linkBatch = $this->processTitlesArray( $titles );
  729. if ( $linkBatch->isEmpty() ) {
  730. // There might be special-page redirects
  731. $this->resolvePendingRedirects();
  732. return;
  733. }
  734. $db = $this->getDB();
  735. $set = $linkBatch->constructSet( 'page', $db );
  736. // Get pageIDs data from the `page` table
  737. $res = $db->select( 'page', $this->getPageTableFields(), $set,
  738. __METHOD__ );
  739. // Hack: get the ns:titles stored in [ ns => [ titles ] ] format
  740. $this->initFromQueryResult( $res, $linkBatch->data, true ); // process Titles
  741. // Resolve any found redirects
  742. $this->resolvePendingRedirects();
  743. }
  744. /**
  745. * Does the same as initFromTitles(), but is based on page IDs instead
  746. * @param array $pageids Array of page IDs
  747. * @param bool $filterIds Whether the IDs need filtering
  748. */
  749. private function initFromPageIds( $pageids, $filterIds = true ) {
  750. if ( !$pageids ) {
  751. return;
  752. }
  753. $pageids = array_map( 'intval', $pageids ); // paranoia
  754. $remaining = array_flip( $pageids );
  755. if ( $filterIds ) {
  756. $pageids = $this->filterIDs( [ [ 'page', 'page_id' ] ], $pageids );
  757. }
  758. $res = null;
  759. if ( !empty( $pageids ) ) {
  760. $set = [
  761. 'page_id' => $pageids
  762. ];
  763. $db = $this->getDB();
  764. // Get pageIDs data from the `page` table
  765. $res = $db->select( 'page', $this->getPageTableFields(), $set,
  766. __METHOD__ );
  767. }
  768. $this->initFromQueryResult( $res, $remaining, false ); // process PageIDs
  769. // Resolve any found redirects
  770. $this->resolvePendingRedirects();
  771. }
  772. /**
  773. * Iterate through the result of the query on 'page' table,
  774. * and for each row create and store title object and save any extra fields requested.
  775. * @param IResultWrapper $res DB Query result
  776. * @param array $remaining Array of either pageID or ns/title elements (optional).
  777. * If given, any missing items will go to $mMissingPageIDs and $mMissingTitles
  778. * @param bool $processTitles Must be provided together with $remaining.
  779. * If true, treat $remaining as an array of [ns][title]
  780. * If false, treat it as an array of [pageIDs]
  781. */
  782. private function initFromQueryResult( $res, &$remaining = null, $processTitles = null ) {
  783. if ( !is_null( $remaining ) && is_null( $processTitles ) ) {
  784. ApiBase::dieDebug( __METHOD__, 'Missing $processTitles parameter when $remaining is provided' );
  785. }
  786. $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
  787. $usernames = [];
  788. if ( $res ) {
  789. foreach ( $res as $row ) {
  790. $pageId = (int)$row->page_id;
  791. // Remove found page from the list of remaining items
  792. if ( isset( $remaining ) ) {
  793. if ( $processTitles ) {
  794. unset( $remaining[$row->page_namespace][$row->page_title] );
  795. } else {
  796. unset( $remaining[$pageId] );
  797. }
  798. }
  799. // Store any extra fields requested by modules
  800. $this->processDbRow( $row );
  801. // Need gender information
  802. if ( $nsInfo->hasGenderDistinction( $row->page_namespace ) ) {
  803. $usernames[] = $row->page_title;
  804. }
  805. }
  806. }
  807. if ( isset( $remaining ) ) {
  808. // Any items left in the $remaining list are added as missing
  809. if ( $processTitles ) {
  810. // The remaining titles in $remaining are non-existent pages
  811. $linkCache = MediaWikiServices::getInstance()->getLinkCache();
  812. foreach ( $remaining as $ns => $dbkeys ) {
  813. foreach ( array_keys( $dbkeys ) as $dbkey ) {
  814. $title = Title::makeTitle( $ns, $dbkey );
  815. $linkCache->addBadLinkObj( $title );
  816. $this->mAllPages[$ns][$dbkey] = $this->mFakePageId;
  817. $this->mMissingPages[$ns][$dbkey] = $this->mFakePageId;
  818. $this->mGoodAndMissingPages[$ns][$dbkey] = $this->mFakePageId;
  819. $this->mMissingTitles[$this->mFakePageId] = $title;
  820. $this->mFakePageId--;
  821. $this->mTitles[] = $title;
  822. // need gender information
  823. if ( $nsInfo->hasGenderDistinction( $ns ) ) {
  824. $usernames[] = $dbkey;
  825. }
  826. }
  827. }
  828. } else {
  829. // The remaining pageids do not exist
  830. if ( !$this->mMissingPageIDs ) {
  831. $this->mMissingPageIDs = array_keys( $remaining );
  832. } else {
  833. $this->mMissingPageIDs = array_merge( $this->mMissingPageIDs, array_keys( $remaining ) );
  834. }
  835. }
  836. }
  837. // Get gender information
  838. $genderCache = MediaWikiServices::getInstance()->getGenderCache();
  839. $genderCache->doQuery( $usernames, __METHOD__ );
  840. }
  841. /**
  842. * Does the same as initFromTitles(), but is based on revision IDs
  843. * instead
  844. * @param array $revids Array of revision IDs
  845. */
  846. private function initFromRevIDs( $revids ) {
  847. if ( !$revids ) {
  848. return;
  849. }
  850. $revids = array_map( 'intval', $revids ); // paranoia
  851. $db = $this->getDB();
  852. $pageids = [];
  853. $remaining = array_flip( $revids );
  854. $revids = $this->filterIDs( [ [ 'revision', 'rev_id' ], [ 'archive', 'ar_rev_id' ] ], $revids );
  855. $goodRemaining = array_flip( $revids );
  856. if ( $revids ) {
  857. $tables = [ 'revision', 'page' ];
  858. $fields = [ 'rev_id', 'rev_page' ];
  859. $where = [ 'rev_id' => $revids, 'rev_page = page_id' ];
  860. // Get pageIDs data from the `page` table
  861. $res = $db->select( $tables, $fields, $where, __METHOD__ );
  862. foreach ( $res as $row ) {
  863. $revid = (int)$row->rev_id;
  864. $pageid = (int)$row->rev_page;
  865. $this->mGoodRevIDs[$revid] = $pageid;
  866. $this->mLiveRevIDs[$revid] = $pageid;
  867. $pageids[$pageid] = '';
  868. unset( $remaining[$revid] );
  869. unset( $goodRemaining[$revid] );
  870. }
  871. }
  872. // Populate all the page information
  873. $this->initFromPageIds( array_keys( $pageids ), false );
  874. // If the user can see deleted revisions, pull out the corresponding
  875. // titles from the archive table and include them too. We ignore
  876. // ar_page_id because deleted revisions are tied by title, not page_id.
  877. if ( $goodRemaining &&
  878. $this->getPermissionManager()->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
  879. $tables = [ 'archive' ];
  880. $fields = [ 'ar_rev_id', 'ar_namespace', 'ar_title' ];
  881. $where = [ 'ar_rev_id' => array_keys( $goodRemaining ) ];
  882. $res = $db->select( $tables, $fields, $where, __METHOD__ );
  883. $titles = [];
  884. foreach ( $res as $row ) {
  885. $revid = (int)$row->ar_rev_id;
  886. $titles[$revid] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
  887. unset( $remaining[$revid] );
  888. }
  889. $this->initFromTitles( $titles );
  890. foreach ( $titles as $revid => $title ) {
  891. $ns = $title->getNamespace();
  892. $dbkey = $title->getDBkey();
  893. // Handle converted titles
  894. if ( !isset( $this->mAllPages[$ns][$dbkey] ) &&
  895. isset( $this->mConvertedTitles[$title->getPrefixedText()] )
  896. ) {
  897. $title = Title::newFromText( $this->mConvertedTitles[$title->getPrefixedText()] );
  898. $ns = $title->getNamespace();
  899. $dbkey = $title->getDBkey();
  900. }
  901. if ( isset( $this->mAllPages[$ns][$dbkey] ) ) {
  902. $this->mGoodRevIDs[$revid] = $this->mAllPages[$ns][$dbkey];
  903. $this->mDeletedRevIDs[$revid] = $this->mAllPages[$ns][$dbkey];
  904. } else {
  905. $remaining[$revid] = true;
  906. }
  907. }
  908. }
  909. $this->mMissingRevIDs = array_keys( $remaining );
  910. }
  911. /**
  912. * Resolve any redirects in the result if redirect resolution was
  913. * requested. This function is called repeatedly until all redirects
  914. * have been resolved.
  915. */
  916. private function resolvePendingRedirects() {
  917. if ( $this->mResolveRedirects ) {
  918. $db = $this->getDB();
  919. $pageFlds = $this->getPageTableFields();
  920. // Repeat until all redirects have been resolved
  921. // The infinite loop is prevented by keeping all known pages in $this->mAllPages
  922. while ( $this->mPendingRedirectIDs || $this->mPendingRedirectSpecialPages ) {
  923. // Resolve redirects by querying the pagelinks table, and repeat the process
  924. // Create a new linkBatch object for the next pass
  925. $linkBatch = $this->getRedirectTargets();
  926. if ( $linkBatch->isEmpty() ) {
  927. break;
  928. }
  929. $set = $linkBatch->constructSet( 'page', $db );
  930. if ( $set === false ) {
  931. break;
  932. }
  933. // Get pageIDs data from the `page` table
  934. $res = $db->select( 'page', $pageFlds, $set, __METHOD__ );
  935. // Hack: get the ns:titles stored in [ns => array(titles)] format
  936. $this->initFromQueryResult( $res, $linkBatch->data, true );
  937. }
  938. }
  939. }
  940. /**
  941. * Get the targets of the pending redirects from the database
  942. *
  943. * Also creates entries in the redirect table for redirects that don't
  944. * have one.
  945. * @return LinkBatch
  946. */
  947. private function getRedirectTargets() {
  948. $titlesToResolve = [];
  949. $db = $this->getDB();
  950. if ( $this->mPendingRedirectIDs ) {
  951. $res = $db->select(
  952. 'redirect',
  953. [
  954. 'rd_from',
  955. 'rd_namespace',
  956. 'rd_fragment',
  957. 'rd_interwiki',
  958. 'rd_title'
  959. ], [ 'rd_from' => array_keys( $this->mPendingRedirectIDs ) ],
  960. __METHOD__
  961. );
  962. foreach ( $res as $row ) {
  963. $rdfrom = (int)$row->rd_from;
  964. $from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText();
  965. $to = Title::makeTitle(
  966. $row->rd_namespace,
  967. $row->rd_title,
  968. $row->rd_fragment,
  969. $row->rd_interwiki
  970. );
  971. $this->mResolvedRedirectTitles[$from] = $this->mPendingRedirectIDs[$rdfrom];
  972. unset( $this->mPendingRedirectIDs[$rdfrom] );
  973. if ( $to->isExternal() ) {
  974. $this->mInterwikiTitles[$to->getPrefixedText()] = $to->getInterwiki();
  975. } elseif ( !isset( $this->mAllPages[$to->getNamespace()][$to->getDBkey()] ) ) {
  976. $titlesToResolve[] = $to;
  977. }
  978. $this->mRedirectTitles[$from] = $to;
  979. }
  980. if ( $this->mPendingRedirectIDs ) {
  981. // We found pages that aren't in the redirect table
  982. // Add them
  983. foreach ( $this->mPendingRedirectIDs as $id => $title ) {
  984. $page = WikiPage::factory( $title );
  985. $rt = $page->insertRedirect();
  986. if ( !$rt ) {
  987. // What the hell. Let's just ignore this
  988. continue;
  989. }
  990. if ( $rt->isExternal() ) {
  991. $this->mInterwikiTitles[$rt->getPrefixedText()] = $rt->getInterwiki();
  992. } elseif ( !isset( $this->mAllPages[$rt->getNamespace()][$rt->getDBkey()] ) ) {
  993. $titlesToResolve[] = $rt;
  994. }
  995. $from = $title->getPrefixedText();
  996. $this->mResolvedRedirectTitles[$from] = $title;
  997. $this->mRedirectTitles[$from] = $rt;
  998. unset( $this->mPendingRedirectIDs[$id] );
  999. }
  1000. }
  1001. }
  1002. if ( $this->mPendingRedirectSpecialPages ) {
  1003. foreach ( $this->mPendingRedirectSpecialPages as $key => list( $from, $to ) ) {
  1004. $fromKey = $from->getPrefixedText();
  1005. $this->mResolvedRedirectTitles[$fromKey] = $from;
  1006. $this->mRedirectTitles[$fromKey] = $to;
  1007. if ( $to->isExternal() ) {
  1008. $this->mInterwikiTitles[$to->getPrefixedText()] = $to->getInterwiki();
  1009. } elseif ( !isset( $this->mAllPages[$to->getNamespace()][$to->getDBkey()] ) ) {
  1010. $titlesToResolve[] = $to;
  1011. }
  1012. }
  1013. $this->mPendingRedirectSpecialPages = [];
  1014. // Set private caching since we don't know what criteria the
  1015. // special pages used to decide on these redirects.
  1016. $this->mCacheMode = 'private';
  1017. }
  1018. return $this->processTitlesArray( $titlesToResolve );
  1019. }
  1020. /**
  1021. * Get the cache mode for the data generated by this module.
  1022. * All PageSet users should take into account whether this returns a more-restrictive
  1023. * cache mode than the using module itself. For possible return values and other
  1024. * details about cache modes, see ApiMain::setCacheMode()
  1025. *
  1026. * Public caching will only be allowed if *all* the modules that supply
  1027. * data for a given request return a cache mode of public.
  1028. *
  1029. * @param array|null $params
  1030. * @return string
  1031. * @since 1.21
  1032. */
  1033. public function getCacheMode( $params = null ) {
  1034. return $this->mCacheMode;
  1035. }
  1036. /**
  1037. * Given an array of title strings, convert them into Title objects.
  1038. * Alternatively, an array of Title objects may be given.
  1039. * This method validates access rights for the title,
  1040. * and appends normalization values to the output.
  1041. *
  1042. * @param array $titles Array of Title objects or strings
  1043. * @return LinkBatch
  1044. */
  1045. private function processTitlesArray( $titles ) {
  1046. $usernames = [];
  1047. $linkBatch = new LinkBatch();
  1048. $services = MediaWikiServices::getInstance();
  1049. $contLang = $services->getContentLanguage();
  1050. $titleObjects = [];
  1051. foreach ( $titles as $index => $title ) {
  1052. if ( is_string( $title ) ) {
  1053. try {
  1054. $titleObj = Title::newFromTextThrow( $title, $this->mDefaultNamespace );
  1055. } catch ( MalformedTitleException $ex ) {
  1056. // Handle invalid titles gracefully
  1057. if ( !isset( $this->mAllPages[0][$title] ) ) {
  1058. $this->mAllPages[0][$title] = $this->mFakePageId;
  1059. $this->mInvalidTitles[$this->mFakePageId] = [
  1060. 'title' => $title,
  1061. 'invalidreason' => $this->getErrorFormatter()->formatException( $ex, [ 'bc' => true ] ),
  1062. ];
  1063. $this->mFakePageId--;
  1064. }
  1065. continue; // There's nothing else we can do
  1066. }
  1067. } else {
  1068. $titleObj = $title;
  1069. }
  1070. $titleObjects[$index] = $titleObj;
  1071. }
  1072. // Get gender information
  1073. $genderCache = $services->getGenderCache();
  1074. $genderCache->doTitlesArray( $titleObjects, __METHOD__ );
  1075. foreach ( $titleObjects as $index => $titleObj ) {
  1076. $title = is_string( $titles[$index] ) ? $titles[$index] : false;
  1077. $unconvertedTitle = $titleObj->getPrefixedText();
  1078. $titleWasConverted = false;
  1079. if ( $titleObj->isExternal() ) {
  1080. // This title is an interwiki link.
  1081. $this->mInterwikiTitles[$unconvertedTitle] = $titleObj->getInterwiki();
  1082. } else {
  1083. // Variants checking
  1084. if (
  1085. $this->mConvertTitles && $contLang->hasVariants() && !$titleObj->exists()
  1086. ) {
  1087. // Language::findVariantLink will modify titleText and titleObj into
  1088. // the canonical variant if possible
  1089. $titleText = $title !== false ? $title : $titleObj->getPrefixedText();
  1090. $contLang->findVariantLink( $titleText, $titleObj );
  1091. $titleWasConverted = $unconvertedTitle !== $titleObj->getPrefixedText();
  1092. }
  1093. if ( $titleObj->getNamespace() < 0 ) {
  1094. // Handle Special and Media pages
  1095. $titleObj = $titleObj->fixSpecialName();
  1096. $ns = $titleObj->getNamespace();
  1097. $dbkey = $titleObj->getDBkey();
  1098. if ( !isset( $this->mAllSpecials[$ns][$dbkey] ) ) {
  1099. $this->mAllSpecials[$ns][$dbkey] = $this->mFakePageId;
  1100. $target = null;
  1101. if ( $ns === NS_SPECIAL && $this->mResolveRedirects ) {
  1102. $spFactory = $services->getSpecialPageFactory();
  1103. $special = $spFactory->getPage( $dbkey );
  1104. if ( $special instanceof RedirectSpecialArticle ) {
  1105. // Only RedirectSpecialArticle is intended to redirect to an article, other kinds of
  1106. // RedirectSpecialPage are probably applying weird URL parameters we don't want to handle.
  1107. $context = new DerivativeContext( $this );
  1108. $context->setTitle( $titleObj );
  1109. $context->setRequest( new FauxRequest );
  1110. $special->setContext( $context );
  1111. list( /* $alias */, $subpage ) = $spFactory->resolveAlias( $dbkey );
  1112. $target = $special->getRedirect( $subpage );
  1113. }
  1114. }
  1115. if ( $target ) {
  1116. $this->mPendingRedirectSpecialPages[$dbkey] = [ $titleObj, $target ];
  1117. } else {
  1118. $this->mSpecialTitles[$this->mFakePageId] = $titleObj;
  1119. $this->mFakePageId--;
  1120. }
  1121. }
  1122. } else {
  1123. // Regular page
  1124. $linkBatch->addObj( $titleObj );
  1125. }
  1126. }
  1127. // Make sure we remember the original title that was
  1128. // given to us. This way the caller can correlate new
  1129. // titles with the originally requested when e.g. the
  1130. // namespace is localized or the capitalization is
  1131. // different
  1132. if ( $titleWasConverted ) {
  1133. $this->mConvertedTitles[$unconvertedTitle] = $titleObj->getPrefixedText();
  1134. // In this case the page can't be Special.
  1135. if ( $title !== false && $title !== $unconvertedTitle ) {
  1136. $this->mNormalizedTitles[$title] = $unconvertedTitle;
  1137. }
  1138. } elseif ( $title !== false && $title !== $titleObj->getPrefixedText() ) {
  1139. $this->mNormalizedTitles[$title] = $titleObj->getPrefixedText();
  1140. }
  1141. }
  1142. return $linkBatch;
  1143. }
  1144. /**
  1145. * Set data for a title.
  1146. *
  1147. * This data may be extracted into an ApiResult using
  1148. * self::populateGeneratorData. This should generally be limited to
  1149. * data that is likely to be particularly useful to end users rather than
  1150. * just being a dump of everything returned in non-generator mode.
  1151. *
  1152. * Redirects here will *not* be followed, even if 'redirects' was
  1153. * specified, since in the case of multiple redirects we can't know which
  1154. * source's data to use on the target.
  1155. *
  1156. * @param Title $title
  1157. * @param array $data
  1158. */
  1159. public function setGeneratorData( Title $title, array $data ) {
  1160. $ns = $title->getNamespace();
  1161. $dbkey = $title->getDBkey();
  1162. $this->mGeneratorData[$ns][$dbkey] = $data;
  1163. }
  1164. /**
  1165. * Controls how generator data about a redirect source is merged into
  1166. * the generator data for the redirect target. When not set no data
  1167. * is merged. Note that if multiple titles redirect to the same target
  1168. * the order of operations is undefined.
  1169. *
  1170. * Example to include generated data from redirect in target, prefering
  1171. * the data generated for the destination when there is a collision:
  1172. * @code
  1173. * $pageSet->setRedirectMergePolicy( function( array $current, array $new ) {
  1174. * return $current + $new;
  1175. * } );
  1176. * @endcode
  1177. *
  1178. * @param callable|null $callable Recieves two array arguments, first the
  1179. * generator data for the redirect target and second the generator data
  1180. * for the redirect source. Returns the resulting generator data to use
  1181. * for the redirect target.
  1182. */
  1183. public function setRedirectMergePolicy( $callable ) {
  1184. $this->mRedirectMergePolicy = $callable;
  1185. }
  1186. /**
  1187. * Populate the generator data for all titles in the result
  1188. *
  1189. * The page data may be inserted into an ApiResult object or into an
  1190. * associative array. The $path parameter specifies the path within the
  1191. * ApiResult or array to find the "pages" node.
  1192. *
  1193. * The "pages" node itself must be an associative array mapping the page ID
  1194. * or fake page ID values returned by this pageset (see
  1195. * self::getAllTitlesByNamespace() and self::getSpecialTitles()) to
  1196. * associative arrays of page data. Each of those subarrays will have the
  1197. * data from self::setGeneratorData() merged in.
  1198. *
  1199. * Data that was set by self::setGeneratorData() for pages not in the
  1200. * "pages" node will be ignored.
  1201. *
  1202. * @param ApiResult|array &$result
  1203. * @param array $path
  1204. * @return bool Whether the data fit
  1205. */
  1206. public function populateGeneratorData( &$result, array $path = [] ) {
  1207. if ( $result instanceof ApiResult ) {
  1208. $data = $result->getResultData( $path );
  1209. if ( $data === null ) {
  1210. return true;
  1211. }
  1212. } else {
  1213. $data = &$result;
  1214. foreach ( $path as $key ) {
  1215. if ( !isset( $data[$key] ) ) {
  1216. // Path isn't in $result, so nothing to add, so everything
  1217. // "fits"
  1218. return true;
  1219. }
  1220. $data = &$data[$key];
  1221. }
  1222. }
  1223. foreach ( $this->mGeneratorData as $ns => $dbkeys ) {
  1224. if ( $ns === NS_SPECIAL ) {
  1225. $pages = [];
  1226. foreach ( $this->mSpecialTitles as $id => $title ) {
  1227. $pages[$title->getDBkey()] = $id;
  1228. }
  1229. } else {
  1230. if ( !isset( $this->mAllPages[$ns] ) ) {
  1231. // No known titles in the whole namespace. Skip it.
  1232. continue;
  1233. }
  1234. $pages = $this->mAllPages[$ns];
  1235. }
  1236. foreach ( $dbkeys as $dbkey => $genData ) {
  1237. if ( !isset( $pages[$dbkey] ) ) {
  1238. // Unknown title. Forget it.
  1239. continue;
  1240. }
  1241. $pageId = $pages[$dbkey];
  1242. if ( !isset( $data[$pageId] ) ) {
  1243. // $pageId didn't make it into the result. Ignore it.
  1244. continue;
  1245. }
  1246. if ( $result instanceof ApiResult ) {
  1247. $path2 = array_merge( $path, [ $pageId ] );
  1248. foreach ( $genData as $key => $value ) {
  1249. if ( !$result->addValue( $path2, $key, $value ) ) {
  1250. return false;
  1251. }
  1252. }
  1253. } else {
  1254. $data[$pageId] = array_merge( $data[$pageId], $genData );
  1255. }
  1256. }
  1257. }
  1258. // Merge data generated about redirect titles into the redirect destination
  1259. if ( $this->mRedirectMergePolicy ) {
  1260. foreach ( $this->mResolvedRedirectTitles as $titleFrom ) {
  1261. $dest = $titleFrom;
  1262. while ( isset( $this->mRedirectTitles[$dest->getPrefixedText()] ) ) {
  1263. $dest = $this->mRedirectTitles[$dest->getPrefixedText()];
  1264. }
  1265. $fromNs = $titleFrom->getNamespace();
  1266. $fromDBkey = $titleFrom->getDBkey();
  1267. $toPageId = $dest->getArticleID();
  1268. if ( isset( $data[$toPageId] ) &&
  1269. isset( $this->mGeneratorData[$fromNs][$fromDBkey] )
  1270. ) {
  1271. // It is necessary to set both $data and add to $result, if an ApiResult,
  1272. // to ensure multiple redirects to the same destination are all merged.
  1273. $data[$toPageId] = call_user_func(
  1274. $this->mRedirectMergePolicy,
  1275. $data[$toPageId],
  1276. $this->mGeneratorData[$fromNs][$fromDBkey]
  1277. );
  1278. if ( $result instanceof ApiResult &&
  1279. !$result->addValue( $path, $toPageId, $data[$toPageId], ApiResult::OVERRIDE )
  1280. ) {
  1281. return false;
  1282. }
  1283. }
  1284. }
  1285. }
  1286. return true;
  1287. }
  1288. /**
  1289. * Get the database connection (read-only)
  1290. * @return IDatabase
  1291. */
  1292. protected function getDB() {
  1293. return $this->mDbSource->getDB();
  1294. }
  1295. public function getAllowedParams( $flags = 0 ) {
  1296. $result = [
  1297. 'titles' => [
  1298. ApiBase::PARAM_ISMULTI => true,
  1299. ApiBase::PARAM_HELP_MSG => 'api-pageset-param-titles',
  1300. ],
  1301. 'pageids' => [
  1302. ApiBase::PARAM_TYPE => 'integer',
  1303. ApiBase::PARAM_ISMULTI => true,
  1304. ApiBase::PARAM_HELP_MSG => 'api-pageset-param-pageids',
  1305. ],
  1306. 'revids' => [
  1307. ApiBase::PARAM_TYPE => 'integer',
  1308. ApiBase::PARAM_ISMULTI => true,
  1309. ApiBase::PARAM_HELP_MSG => 'api-pageset-param-revids',
  1310. ],
  1311. 'generator' => [
  1312. ApiBase::PARAM_TYPE => null,
  1313. ApiBase::PARAM_HELP_MSG => 'api-pageset-param-generator',
  1314. ApiBase::PARAM_SUBMODULE_PARAM_PREFIX => 'g',
  1315. ],
  1316. 'redirects' => [
  1317. ApiBase::PARAM_DFLT => false,
  1318. ApiBase::PARAM_HELP_MSG => $this->mAllowGenerator
  1319. ? 'api-pageset-param-redirects-generator'
  1320. : 'api-pageset-param-redirects-nogenerator',
  1321. ],
  1322. 'converttitles' => [
  1323. ApiBase::PARAM_DFLT => false,
  1324. ApiBase::PARAM_HELP_MSG => [
  1325. 'api-pageset-param-converttitles',
  1326. [ Message::listParam( LanguageConverter::$languagesWithVariants, 'text' ) ],
  1327. ],
  1328. ],
  1329. ];
  1330. if ( !$this->mAllowGenerator ) {
  1331. unset( $result['generator'] );
  1332. } elseif ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
  1333. $result['generator'][ApiBase::PARAM_TYPE] = 'submodule';
  1334. $result['generator'][ApiBase::PARAM_SUBMODULE_MAP] = $this->getGenerators();
  1335. }
  1336. return $result;
  1337. }
  1338. protected function handleParamNormalization( $paramName, $value, $rawValue ) {
  1339. parent::handleParamNormalization( $paramName, $value, $rawValue );
  1340. if ( $paramName === 'titles' ) {
  1341. // For the 'titles' parameter, we want to split it like ApiBase would
  1342. // and add any changed titles to $this->mNormalizedTitles
  1343. $value = $this->explodeMultiValue( $value, self::LIMIT_SML2 + 1 );
  1344. $l = count( $value );
  1345. $rawValue = $this->explodeMultiValue( $rawValue, $l );
  1346. for ( $i = 0; $i < $l; $i++ ) {
  1347. if ( $value[$i] !== $rawValue[$i] ) {
  1348. $this->mNormalizedTitles[$rawValue[$i]] = $value[$i];
  1349. }
  1350. }
  1351. }
  1352. }
  1353. private static $generators = null;
  1354. /**
  1355. * Get an array of all available generators
  1356. * @return array
  1357. */
  1358. private function getGenerators() {
  1359. if ( self::$generators === null ) {
  1360. $query = $this->mDbSource;
  1361. if ( !( $query instanceof ApiQuery ) ) {
  1362. // If the parent container of this pageset is not ApiQuery,
  1363. // we must create it to get module manager
  1364. $query = $this->getMain()->getModuleManager()->getModule( 'query' );
  1365. }
  1366. $gens = [];
  1367. $prefix = $query->getModulePath() . '+';
  1368. $mgr = $query->getModuleManager();
  1369. foreach ( $mgr->getNamesWithClasses() as $name => $class ) {
  1370. if ( is_subclass_of( $class, ApiQueryGeneratorBase::class ) ) {
  1371. $gens[$name] = $prefix . $name;
  1372. }
  1373. }
  1374. ksort( $gens );
  1375. self::$generators = $gens;
  1376. }
  1377. return self::$generators;
  1378. }
  1379. }