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