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