Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
74.15% |
175 / 236 |
|
35.71% |
5 / 14 |
CRAP | |
0.00% |
0 / 1 |
ApiQuery | |
74.15% |
175 / 236 |
|
35.71% |
5 / 14 |
144.52 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
1 | |||
getModuleManager | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPageSet | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCustomPrinter | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
execute | |
82.22% |
37 / 45 |
|
0.00% |
0 / 1 |
7.28 | |||
mergeCacheMode | |
50.00% |
3 / 6 |
|
0.00% |
0 / 1 |
6.00 | |||
instantiateModules | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
7.39 | |||
outputGeneralPageInfo | |
67.19% |
43 / 64 |
|
0.00% |
0 / 1 |
44.35 | |||
doExport | |
71.43% |
15 / 21 |
|
0.00% |
0 / 1 |
6.84 | |||
getAllowedParams | |
96.55% |
28 / 29 |
|
0.00% |
0 / 1 |
2 | |||
isReadMode | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
6 | |||
isWriteMode | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
getExamplesMessages | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Copyright © 2006 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\Export\WikiExporterFactory; |
24 | use MediaWiki\MainConfigNames; |
25 | use MediaWiki\MediaWikiServices; |
26 | use MediaWiki\Title\Title; |
27 | use MediaWiki\Title\TitleFactory; |
28 | use MediaWiki\Title\TitleFormatter; |
29 | use Wikimedia\ObjectFactory\ObjectFactory; |
30 | use Wikimedia\ParamValidator\ParamValidator; |
31 | |
32 | /** |
33 | * This is the main query class. It behaves similar to ApiMain: based on the |
34 | * parameters given, it will create a list of titles to work on (an ApiPageSet |
35 | * object), instantiate and execute various property/list/meta modules, and |
36 | * assemble all resulting data into a single ApiResult object. |
37 | * |
38 | * In generator mode, a generator will be executed first to populate a second |
39 | * ApiPageSet object, and that object will be used for all subsequent modules. |
40 | * |
41 | * @ingroup API |
42 | */ |
43 | class ApiQuery extends ApiBase { |
44 | |
45 | /** |
46 | * List of Api Query prop modules |
47 | */ |
48 | private const QUERY_PROP_MODULES = [ |
49 | 'categories' => [ |
50 | 'class' => ApiQueryCategories::class, |
51 | ], |
52 | 'categoryinfo' => [ |
53 | 'class' => ApiQueryCategoryInfo::class, |
54 | ], |
55 | 'contributors' => [ |
56 | 'class' => ApiQueryContributors::class, |
57 | 'services' => [ |
58 | 'RevisionStore', |
59 | 'ActorMigration', |
60 | 'UserGroupManager', |
61 | 'GroupPermissionsLookup', |
62 | 'TempUserConfig' |
63 | ] |
64 | ], |
65 | 'deletedrevisions' => [ |
66 | 'class' => ApiQueryDeletedRevisions::class, |
67 | 'services' => [ |
68 | 'RevisionStore', |
69 | 'ContentHandlerFactory', |
70 | 'ParserFactory', |
71 | 'SlotRoleRegistry', |
72 | 'ChangeTagDefStore', |
73 | 'LinkBatchFactory', |
74 | 'ContentRenderer', |
75 | 'ContentTransformer', |
76 | 'CommentFormatter', |
77 | 'TempUserCreator', |
78 | 'UserFactory', |
79 | ] |
80 | ], |
81 | 'duplicatefiles' => [ |
82 | 'class' => ApiQueryDuplicateFiles::class, |
83 | 'services' => [ |
84 | 'RepoGroup', |
85 | ] |
86 | ], |
87 | 'extlinks' => [ |
88 | 'class' => ApiQueryExternalLinks::class, |
89 | 'services' => [ |
90 | 'UrlUtils', |
91 | ], |
92 | ], |
93 | 'fileusage' => [ |
94 | 'class' => ApiQueryBacklinksprop::class, |
95 | 'services' => [ |
96 | // Same as for linkshere, redirects, transcludedin |
97 | 'LinksMigration', |
98 | ] |
99 | ], |
100 | 'images' => [ |
101 | 'class' => ApiQueryImages::class, |
102 | ], |
103 | 'imageinfo' => [ |
104 | 'class' => ApiQueryImageInfo::class, |
105 | 'services' => [ |
106 | // Same as for stashimageinfo |
107 | 'RepoGroup', |
108 | 'ContentLanguage', |
109 | 'BadFileLookup', |
110 | ] |
111 | ], |
112 | 'info' => [ |
113 | 'class' => ApiQueryInfo::class, |
114 | 'services' => [ |
115 | 'ContentLanguage', |
116 | 'LinkBatchFactory', |
117 | 'NamespaceInfo', |
118 | 'TitleFactory', |
119 | 'TitleFormatter', |
120 | 'WatchedItemStore', |
121 | 'LanguageConverterFactory', |
122 | 'RestrictionStore', |
123 | 'LinksMigration', |
124 | 'TempUserCreator', |
125 | 'UserFactory', |
126 | 'IntroMessageBuilder', |
127 | 'PreloadedContentBuilder', |
128 | 'RevisionLookup', |
129 | 'UrlUtils', |
130 | ], |
131 | ], |
132 | 'links' => [ |
133 | 'class' => ApiQueryLinks::class, |
134 | 'services' => [ |
135 | // Same as for templates |
136 | 'LinkBatchFactory', |
137 | 'LinksMigration', |
138 | ] |
139 | ], |
140 | 'linkshere' => [ |
141 | 'class' => ApiQueryBacklinksprop::class, |
142 | 'services' => [ |
143 | // Same as for fileusage, redirects, transcludedin |
144 | 'LinksMigration', |
145 | ] |
146 | ], |
147 | 'iwlinks' => [ |
148 | 'class' => ApiQueryIWLinks::class, |
149 | 'services' => [ |
150 | 'UrlUtils', |
151 | ] |
152 | ], |
153 | 'langlinks' => [ |
154 | 'class' => ApiQueryLangLinks::class, |
155 | 'services' => [ |
156 | 'LanguageNameUtils', |
157 | 'ContentLanguage', |
158 | 'UrlUtils', |
159 | ] |
160 | ], |
161 | 'pageprops' => [ |
162 | 'class' => ApiQueryPageProps::class, |
163 | 'services' => [ |
164 | 'PageProps', |
165 | ] |
166 | ], |
167 | 'redirects' => [ |
168 | 'class' => ApiQueryBacklinksprop::class, |
169 | 'services' => [ |
170 | // Same as for fileusage, linkshere, transcludedin |
171 | 'LinksMigration', |
172 | ] |
173 | ], |
174 | 'revisions' => [ |
175 | 'class' => ApiQueryRevisions::class, |
176 | 'services' => [ |
177 | 'RevisionStore', |
178 | 'ContentHandlerFactory', |
179 | 'ParserFactory', |
180 | 'SlotRoleRegistry', |
181 | 'ChangeTagDefStore', |
182 | 'ActorMigration', |
183 | 'ContentRenderer', |
184 | 'ContentTransformer', |
185 | 'CommentFormatter', |
186 | 'TempUserCreator', |
187 | 'UserFactory', |
188 | ] |
189 | ], |
190 | 'stashimageinfo' => [ |
191 | 'class' => ApiQueryStashImageInfo::class, |
192 | 'services' => [ |
193 | // Same as for imageinfo |
194 | 'RepoGroup', |
195 | 'ContentLanguage', |
196 | 'BadFileLookup', |
197 | ] |
198 | ], |
199 | 'templates' => [ |
200 | 'class' => ApiQueryLinks::class, |
201 | 'services' => [ |
202 | // Same as for links |
203 | 'LinkBatchFactory', |
204 | 'LinksMigration', |
205 | ] |
206 | ], |
207 | 'transcludedin' => [ |
208 | 'class' => ApiQueryBacklinksprop::class, |
209 | 'services' => [ |
210 | // Same as for fileusage, linkshere, redirects |
211 | 'LinksMigration', |
212 | ] |
213 | ], |
214 | ]; |
215 | |
216 | /** |
217 | * List of Api Query list modules |
218 | */ |
219 | private const QUERY_LIST_MODULES = [ |
220 | 'allcategories' => [ |
221 | 'class' => ApiQueryAllCategories::class, |
222 | ], |
223 | 'alldeletedrevisions' => [ |
224 | 'class' => ApiQueryAllDeletedRevisions::class, |
225 | 'services' => [ |
226 | 'RevisionStore', |
227 | 'ContentHandlerFactory', |
228 | 'ParserFactory', |
229 | 'SlotRoleRegistry', |
230 | 'ChangeTagDefStore', |
231 | 'NamespaceInfo', |
232 | 'ContentRenderer', |
233 | 'ContentTransformer', |
234 | 'CommentFormatter', |
235 | 'TempUserCreator', |
236 | 'UserFactory', |
237 | ] |
238 | ], |
239 | 'allfileusages' => [ |
240 | 'class' => ApiQueryAllLinks::class, |
241 | 'services' => [ |
242 | // Same as for alllinks, allredirects, alltransclusions |
243 | 'NamespaceInfo', |
244 | 'GenderCache', |
245 | 'LinksMigration', |
246 | ] |
247 | ], |
248 | 'allimages' => [ |
249 | 'class' => ApiQueryAllImages::class, |
250 | 'services' => [ |
251 | 'RepoGroup', |
252 | 'GroupPermissionsLookup', |
253 | ] |
254 | ], |
255 | 'alllinks' => [ |
256 | 'class' => ApiQueryAllLinks::class, |
257 | 'services' => [ |
258 | // Same as for allfileusages, allredirects, alltransclusions |
259 | 'NamespaceInfo', |
260 | 'GenderCache', |
261 | 'LinksMigration', |
262 | ] |
263 | ], |
264 | 'allpages' => [ |
265 | 'class' => ApiQueryAllPages::class, |
266 | 'services' => [ |
267 | 'NamespaceInfo', |
268 | 'GenderCache', |
269 | 'RestrictionStore', |
270 | ] |
271 | ], |
272 | 'allredirects' => [ |
273 | 'class' => ApiQueryAllLinks::class, |
274 | 'services' => [ |
275 | // Same as for allfileusages, alllinks, alltransclusions |
276 | 'NamespaceInfo', |
277 | 'GenderCache', |
278 | 'LinksMigration', |
279 | ] |
280 | ], |
281 | 'allrevisions' => [ |
282 | 'class' => ApiQueryAllRevisions::class, |
283 | 'services' => [ |
284 | 'RevisionStore', |
285 | 'ContentHandlerFactory', |
286 | 'ParserFactory', |
287 | 'SlotRoleRegistry', |
288 | 'ActorMigration', |
289 | 'NamespaceInfo', |
290 | 'ContentRenderer', |
291 | 'ContentTransformer', |
292 | 'CommentFormatter', |
293 | 'TempUserCreator', |
294 | 'UserFactory', |
295 | ] |
296 | ], |
297 | 'mystashedfiles' => [ |
298 | 'class' => ApiQueryMyStashedFiles::class, |
299 | ], |
300 | 'alltransclusions' => [ |
301 | 'class' => ApiQueryAllLinks::class, |
302 | 'services' => [ |
303 | // Same as for allfileusages, alllinks, allredirects |
304 | 'NamespaceInfo', |
305 | 'GenderCache', |
306 | 'LinksMigration', |
307 | ] |
308 | ], |
309 | 'allusers' => [ |
310 | 'class' => ApiQueryAllUsers::class, |
311 | 'services' => [ |
312 | 'UserFactory', |
313 | 'UserGroupManager', |
314 | 'GroupPermissionsLookup', |
315 | 'ContentLanguage', |
316 | ] |
317 | ], |
318 | 'backlinks' => [ |
319 | 'class' => ApiQueryBacklinks::class, |
320 | 'services' => [ |
321 | 'LinksMigration', |
322 | ] |
323 | ], |
324 | 'blocks' => [ |
325 | 'class' => ApiQueryBlocks::class, |
326 | 'services' => [ |
327 | 'DatabaseBlockStore', |
328 | 'BlockActionInfo', |
329 | 'BlockRestrictionStore', |
330 | 'CommentStore', |
331 | 'HideUserUtils', |
332 | ], |
333 | ], |
334 | 'categorymembers' => [ |
335 | 'class' => ApiQueryCategoryMembers::class, |
336 | 'services' => [ |
337 | 'CollationFactory', |
338 | ] |
339 | ], |
340 | 'deletedrevs' => [ |
341 | 'class' => ApiQueryDeletedrevs::class, |
342 | 'services' => [ |
343 | 'CommentStore', |
344 | 'RowCommentFormatter', |
345 | 'RevisionStore', |
346 | 'ChangeTagDefStore', |
347 | 'LinkBatchFactory', |
348 | ], |
349 | ], |
350 | 'embeddedin' => [ |
351 | 'class' => ApiQueryBacklinks::class, |
352 | 'services' => [ |
353 | 'LinksMigration', |
354 | ] |
355 | ], |
356 | 'exturlusage' => [ |
357 | 'class' => ApiQueryExtLinksUsage::class, |
358 | 'services' => [ |
359 | 'UrlUtils', |
360 | ], |
361 | ], |
362 | 'filearchive' => [ |
363 | 'class' => ApiQueryFilearchive::class, |
364 | 'services' => [ |
365 | 'CommentStore', |
366 | 'CommentFormatter', |
367 | ], |
368 | ], |
369 | 'imageusage' => [ |
370 | 'class' => ApiQueryBacklinks::class, |
371 | 'services' => [ |
372 | 'LinksMigration', |
373 | ] |
374 | ], |
375 | 'iwbacklinks' => [ |
376 | 'class' => ApiQueryIWBacklinks::class, |
377 | ], |
378 | 'langbacklinks' => [ |
379 | 'class' => ApiQueryLangBacklinks::class, |
380 | ], |
381 | 'logevents' => [ |
382 | 'class' => ApiQueryLogEvents::class, |
383 | 'services' => [ |
384 | 'CommentStore', |
385 | 'RowCommentFormatter', |
386 | 'ChangeTagDefStore', |
387 | 'UserNameUtils', |
388 | ], |
389 | ], |
390 | 'pageswithprop' => [ |
391 | 'class' => ApiQueryPagesWithProp::class, |
392 | ], |
393 | 'pagepropnames' => [ |
394 | 'class' => ApiQueryPagePropNames::class, |
395 | ], |
396 | 'prefixsearch' => [ |
397 | 'class' => ApiQueryPrefixSearch::class, |
398 | 'services' => [ |
399 | 'SearchEngineConfig', |
400 | 'SearchEngineFactory', |
401 | ], |
402 | ], |
403 | 'protectedtitles' => [ |
404 | 'class' => ApiQueryProtectedTitles::class, |
405 | 'services' => [ |
406 | 'CommentStore', |
407 | 'RowCommentFormatter' |
408 | ], |
409 | ], |
410 | 'querypage' => [ |
411 | 'class' => ApiQueryQueryPage::class, |
412 | 'services' => [ |
413 | 'SpecialPageFactory', |
414 | ] |
415 | ], |
416 | 'random' => [ |
417 | 'class' => ApiQueryRandom::class, |
418 | ], |
419 | 'recentchanges' => [ |
420 | 'class' => ApiQueryRecentChanges::class, |
421 | 'services' => [ |
422 | 'CommentStore', |
423 | 'RowCommentFormatter', |
424 | 'ChangeTagDefStore', |
425 | 'SlotRoleStore', |
426 | 'SlotRoleRegistry', |
427 | 'UserNameUtils', |
428 | ], |
429 | ], |
430 | 'search' => [ |
431 | 'class' => ApiQuerySearch::class, |
432 | 'services' => [ |
433 | 'SearchEngineConfig', |
434 | 'SearchEngineFactory', |
435 | 'TitleMatcher', |
436 | ], |
437 | ], |
438 | 'tags' => [ |
439 | 'class' => ApiQueryTags::class, |
440 | 'services' => [ |
441 | 'ChangeTagsStore', |
442 | ] |
443 | ], |
444 | 'usercontribs' => [ |
445 | 'class' => ApiQueryUserContribs::class, |
446 | 'services' => [ |
447 | 'CommentStore', |
448 | 'UserIdentityLookup', |
449 | 'UserNameUtils', |
450 | 'RevisionStore', |
451 | 'ChangeTagDefStore', |
452 | 'ActorMigration', |
453 | 'CommentFormatter', |
454 | ], |
455 | ], |
456 | 'users' => [ |
457 | 'class' => ApiQueryUsers::class, |
458 | 'services' => [ |
459 | 'UserNameUtils', |
460 | 'UserFactory', |
461 | 'UserGroupManager', |
462 | 'GenderCache', |
463 | 'AuthManager', |
464 | ], |
465 | ], |
466 | 'watchlist' => [ |
467 | 'class' => ApiQueryWatchlist::class, |
468 | 'services' => [ |
469 | 'CommentStore', |
470 | 'WatchedItemQueryService', |
471 | 'ContentLanguage', |
472 | 'NamespaceInfo', |
473 | 'GenderCache', |
474 | 'CommentFormatter', |
475 | 'TempUserConfig' |
476 | ], |
477 | ], |
478 | 'watchlistraw' => [ |
479 | 'class' => ApiQueryWatchlistRaw::class, |
480 | 'services' => [ |
481 | 'WatchedItemQueryService', |
482 | 'ContentLanguage', |
483 | 'NamespaceInfo', |
484 | 'GenderCache', |
485 | ] |
486 | ], |
487 | ]; |
488 | |
489 | /** |
490 | * List of Api Query meta modules |
491 | */ |
492 | private const QUERY_META_MODULES = [ |
493 | 'allmessages' => [ |
494 | 'class' => ApiQueryAllMessages::class, |
495 | 'services' => [ |
496 | 'ContentLanguage', |
497 | 'LanguageFactory', |
498 | 'LanguageNameUtils', |
499 | 'LocalisationCache', |
500 | 'MessageCache', |
501 | ] |
502 | ], |
503 | 'authmanagerinfo' => [ |
504 | 'class' => ApiQueryAuthManagerInfo::class, |
505 | 'services' => [ |
506 | 'AuthManager', |
507 | ] |
508 | ], |
509 | 'siteinfo' => [ |
510 | 'class' => ApiQuerySiteinfo::class, |
511 | 'services' => [ |
512 | 'UserOptionsLookup', |
513 | 'UserGroupManager', |
514 | 'LanguageConverterFactory', |
515 | 'LanguageFactory', |
516 | 'LanguageNameUtils', |
517 | 'ContentLanguage', |
518 | 'NamespaceInfo', |
519 | 'InterwikiLookup', |
520 | 'ParserFactory', |
521 | 'MagicWordFactory', |
522 | 'SpecialPageFactory', |
523 | 'SkinFactory', |
524 | 'DBLoadBalancer', |
525 | 'ReadOnlyMode', |
526 | 'UrlUtils', |
527 | ] |
528 | ], |
529 | 'userinfo' => [ |
530 | 'class' => ApiQueryUserInfo::class, |
531 | 'services' => [ |
532 | 'TalkPageNotificationManager', |
533 | 'WatchedItemStore', |
534 | 'UserEditTracker', |
535 | 'UserOptionsLookup', |
536 | 'UserGroupManager', |
537 | ] |
538 | ], |
539 | 'filerepoinfo' => [ |
540 | 'class' => ApiQueryFileRepoInfo::class, |
541 | 'services' => [ |
542 | 'RepoGroup', |
543 | ] |
544 | ], |
545 | 'tokens' => [ |
546 | 'class' => ApiQueryTokens::class, |
547 | ], |
548 | 'languageinfo' => [ |
549 | 'class' => ApiQueryLanguageinfo::class, |
550 | 'services' => [ |
551 | 'LanguageFactory', |
552 | 'LanguageNameUtils', |
553 | 'LanguageFallback', |
554 | 'LanguageConverterFactory', |
555 | ], |
556 | ], |
557 | ]; |
558 | |
559 | /** |
560 | * @var ApiPageSet |
561 | */ |
562 | private $mPageSet; |
563 | |
564 | private $mParams; |
565 | private $mModuleMgr; |
566 | |
567 | private WikiExporterFactory $wikiExporterFactory; |
568 | private TitleFormatter $titleFormatter; |
569 | private TitleFactory $titleFactory; |
570 | |
571 | /** |
572 | * @param ApiMain $main |
573 | * @param string $action |
574 | * @param ObjectFactory $objectFactory |
575 | * @param WikiExporterFactory $wikiExporterFactory |
576 | * @param TitleFormatter $titleFormatter |
577 | * @param TitleFactory $titleFactory |
578 | */ |
579 | public function __construct( |
580 | ApiMain $main, |
581 | $action, |
582 | ObjectFactory $objectFactory, |
583 | WikiExporterFactory $wikiExporterFactory, |
584 | TitleFormatter $titleFormatter, |
585 | TitleFactory $titleFactory |
586 | ) { |
587 | parent::__construct( $main, $action ); |
588 | |
589 | $this->mModuleMgr = new ApiModuleManager( |
590 | $this, |
591 | $objectFactory |
592 | ); |
593 | |
594 | // Allow custom modules to be added in LocalSettings.php |
595 | $config = $this->getConfig(); |
596 | $this->mModuleMgr->addModules( self::QUERY_PROP_MODULES, 'prop' ); |
597 | $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIPropModules ), 'prop' ); |
598 | $this->mModuleMgr->addModules( self::QUERY_LIST_MODULES, 'list' ); |
599 | $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIListModules ), 'list' ); |
600 | $this->mModuleMgr->addModules( self::QUERY_META_MODULES, 'meta' ); |
601 | $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIMetaModules ), 'meta' ); |
602 | |
603 | $this->getHookRunner()->onApiQuery__moduleManager( $this->mModuleMgr ); |
604 | |
605 | // Create PageSet that will process titles/pageids/revids/generator |
606 | $this->mPageSet = new ApiPageSet( $this ); |
607 | $this->wikiExporterFactory = $wikiExporterFactory; |
608 | $this->titleFormatter = $titleFormatter; |
609 | $this->titleFactory = $titleFactory; |
610 | } |
611 | |
612 | /** |
613 | * Overrides to return this instance's module manager. |
614 | * @return ApiModuleManager |
615 | */ |
616 | public function getModuleManager() { |
617 | return $this->mModuleMgr; |
618 | } |
619 | |
620 | /** |
621 | * Gets the set of pages the user has requested (or generated) |
622 | * @return ApiPageSet |
623 | */ |
624 | public function getPageSet() { |
625 | return $this->mPageSet; |
626 | } |
627 | |
628 | /** |
629 | * @return ApiFormatRaw|null |
630 | */ |
631 | public function getCustomPrinter() { |
632 | // If &exportnowrap is set, use the raw formatter |
633 | if ( $this->getParameter( 'export' ) && |
634 | $this->getParameter( 'exportnowrap' ) |
635 | ) { |
636 | return new ApiFormatRaw( $this->getMain(), |
637 | $this->getMain()->createPrinterByName( 'xml' ) ); |
638 | } else { |
639 | return null; |
640 | } |
641 | } |
642 | |
643 | /** |
644 | * Query execution happens in the following steps: |
645 | * #1 Create a PageSet object with any pages requested by the user |
646 | * #2 If using a generator, execute it to get a new ApiPageSet object |
647 | * #3 Instantiate all requested modules. |
648 | * This way the PageSet object will know what shared data is required, |
649 | * and minimize DB calls. |
650 | * #4 Output all normalization and redirect resolution information |
651 | * #5 Execute all requested modules |
652 | */ |
653 | public function execute() { |
654 | $this->mParams = $this->extractRequestParams(); |
655 | |
656 | // Instantiate requested modules |
657 | $allModules = []; |
658 | $this->instantiateModules( $allModules, 'prop' ); |
659 | $propModules = array_keys( $allModules ); |
660 | $this->instantiateModules( $allModules, 'list' ); |
661 | $this->instantiateModules( $allModules, 'meta' ); |
662 | |
663 | // Filter modules based on continue parameter |
664 | $continuationManager = new ApiContinuationManager( $this, $allModules, $propModules ); |
665 | $this->setContinuationManager( $continuationManager ); |
666 | /** @var ApiQueryBase[] $modules */ |
667 | $modules = $continuationManager->getRunModules(); |
668 | '@phan-var ApiQueryBase[] $modules'; |
669 | |
670 | $statsFactory = MediaWikiServices::getInstance()->getStatsFactory(); |
671 | |
672 | if ( !$continuationManager->isGeneratorDone() ) { |
673 | // Query modules may optimize data requests through the $this->getPageSet() |
674 | // object by adding extra fields from the page table. |
675 | foreach ( $modules as $module ) { |
676 | // Augment api-query.$module.executeTiming metric with timings for requestExtraData() |
677 | $timer = $statsFactory->getTiming( 'api_query_extraDataTiming_seconds' ) |
678 | ->setLabel( 'module', $module->getModuleName() ) |
679 | ->copyToStatsdAt( 'api-query.' . $module->getModuleName() . '.extraDataTiming' ); |
680 | $timer->start(); |
681 | $module->requestExtraData( $this->mPageSet ); |
682 | $timer->stop(); |
683 | } |
684 | // Populate page/revision information |
685 | $this->mPageSet->execute(); |
686 | // Record page information (title, namespace, if exists, etc) |
687 | $this->outputGeneralPageInfo(); |
688 | } else { |
689 | $this->mPageSet->executeDryRun(); |
690 | } |
691 | |
692 | $cacheMode = $this->mPageSet->getCacheMode(); |
693 | |
694 | // Execute all unfinished modules |
695 | foreach ( $modules as $module ) { |
696 | // Break down of the api.query.executeTiming metric by query module. |
697 | $timer = $statsFactory->getTiming( 'api_query_executeTiming_seconds' ) |
698 | ->setLabel( 'module', $module->getModuleName() ) |
699 | ->copyToStatsdAt( 'api-query.' . $module->getModuleName() . '.executeTiming' ); |
700 | $timer->start(); |
701 | |
702 | $params = $module->extractRequestParams(); |
703 | $cacheMode = $this->mergeCacheMode( |
704 | $cacheMode, $module->getCacheMode( $params ) ); |
705 | $module->execute(); |
706 | |
707 | $timer->stop(); |
708 | |
709 | $this->getHookRunner()->onAPIQueryAfterExecute( $module ); |
710 | } |
711 | |
712 | // Set the cache mode |
713 | $this->getMain()->setCacheMode( $cacheMode ); |
714 | |
715 | // Write the continuation data into the result |
716 | $this->setContinuationManager( null ); |
717 | if ( $this->mParams['rawcontinue'] ) { |
718 | $data = $continuationManager->getRawNonContinuation(); |
719 | if ( $data ) { |
720 | $this->getResult()->addValue( null, 'query-noncontinue', $data, |
721 | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK ); |
722 | } |
723 | $data = $continuationManager->getRawContinuation(); |
724 | if ( $data ) { |
725 | $this->getResult()->addValue( null, 'query-continue', $data, |
726 | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK ); |
727 | } |
728 | } else { |
729 | $continuationManager->setContinuationIntoResult( $this->getResult() ); |
730 | } |
731 | } |
732 | |
733 | /** |
734 | * Update a cache mode string, applying the cache mode of a new module to it. |
735 | * The cache mode may increase in the level of privacy, but public modules |
736 | * added to private data do not decrease the level of privacy. |
737 | * |
738 | * @param string $cacheMode |
739 | * @param string $modCacheMode |
740 | * @return string |
741 | */ |
742 | protected function mergeCacheMode( $cacheMode, $modCacheMode ) { |
743 | if ( $modCacheMode === 'anon-public-user-private' ) { |
744 | if ( $cacheMode !== 'private' ) { |
745 | $cacheMode = 'anon-public-user-private'; |
746 | } |
747 | } elseif ( $modCacheMode === 'public' ) { |
748 | // do nothing, if it's public already it will stay public |
749 | } else { |
750 | $cacheMode = 'private'; |
751 | } |
752 | |
753 | return $cacheMode; |
754 | } |
755 | |
756 | /** |
757 | * Create instances of all modules requested by the client |
758 | * @param array &$modules To append instantiated modules to |
759 | * @param string $param Parameter name to read modules from |
760 | */ |
761 | private function instantiateModules( &$modules, $param ) { |
762 | $wasPosted = $this->getRequest()->wasPosted(); |
763 | if ( isset( $this->mParams[$param] ) ) { |
764 | foreach ( $this->mParams[$param] as $moduleName ) { |
765 | $instance = $this->mModuleMgr->getModule( $moduleName, $param ); |
766 | if ( $instance === null ) { |
767 | ApiBase::dieDebug( __METHOD__, 'Error instantiating module' ); |
768 | } |
769 | if ( !$wasPosted && $instance->mustBePosted() ) { |
770 | $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $moduleName ] ); |
771 | } |
772 | // Ignore duplicates. TODO 2.0: die()? |
773 | if ( !array_key_exists( $moduleName, $modules ) ) { |
774 | $modules[$moduleName] = $instance; |
775 | } |
776 | } |
777 | } |
778 | } |
779 | |
780 | /** |
781 | * Appends an element for each page in the current pageSet with the |
782 | * most general information (id, title), plus any title normalizations |
783 | * and missing or invalid title/pageids/revids. |
784 | */ |
785 | private function outputGeneralPageInfo() { |
786 | $pageSet = $this->getPageSet(); |
787 | $result = $this->getResult(); |
788 | |
789 | // We can't really handle max-result-size failure here, but we need to |
790 | // check anyway in case someone set the limit stupidly low. |
791 | $fit = true; |
792 | |
793 | $values = $pageSet->getNormalizedTitlesAsResult( $result ); |
794 | if ( $values ) { |
795 | // @phan-suppress-next-line PhanRedundantCondition |
796 | $fit = $fit && $result->addValue( 'query', 'normalized', $values ); |
797 | } |
798 | $values = $pageSet->getConvertedTitlesAsResult( $result ); |
799 | if ( $values ) { |
800 | $fit = $fit && $result->addValue( 'query', 'converted', $values ); |
801 | } |
802 | $values = $pageSet->getInterwikiTitlesAsResult( $result, $this->mParams['iwurl'] ); |
803 | if ( $values ) { |
804 | $fit = $fit && $result->addValue( 'query', 'interwiki', $values ); |
805 | } |
806 | $values = $pageSet->getRedirectTitlesAsResult( $result ); |
807 | if ( $values ) { |
808 | $fit = $fit && $result->addValue( 'query', 'redirects', $values ); |
809 | } |
810 | $values = $pageSet->getMissingRevisionIDsAsResult( $result ); |
811 | if ( $values ) { |
812 | $fit = $fit && $result->addValue( 'query', 'badrevids', $values ); |
813 | } |
814 | |
815 | // Page elements |
816 | // Cannot use ApiPageSet::getInvalidTitlesAndRevisions, it does not set $fakeId |
817 | $pages = []; |
818 | |
819 | // Report any missing titles |
820 | foreach ( $pageSet->getMissingPages() as $fakeId => $page ) { |
821 | $vals = []; |
822 | $vals['ns'] = $page->getNamespace(); |
823 | $vals['title'] = $this->titleFormatter->getPrefixedText( $page ); |
824 | $vals['missing'] = true; |
825 | $title = $this->titleFactory->newFromPageIdentity( $page ); |
826 | if ( $title->isKnown() ) { |
827 | $vals['known'] = true; |
828 | } |
829 | $pages[$fakeId] = $vals; |
830 | } |
831 | // Report any invalid titles |
832 | foreach ( $pageSet->getInvalidTitlesAndReasons() as $fakeId => $data ) { |
833 | $pages[$fakeId] = $data + [ 'invalid' => true ]; |
834 | } |
835 | // Report any missing page ids |
836 | foreach ( $pageSet->getMissingPageIDs() as $pageid ) { |
837 | $pages[$pageid] = [ |
838 | 'pageid' => $pageid, |
839 | 'missing' => true, |
840 | ]; |
841 | } |
842 | // Report special pages |
843 | /** @var \MediaWiki\Page\PageReference $page */ |
844 | foreach ( $pageSet->getSpecialPages() as $fakeId => $page ) { |
845 | $vals = []; |
846 | $vals['ns'] = $page->getNamespace(); |
847 | $vals['title'] = $this->titleFormatter->getPrefixedText( $page ); |
848 | $vals['special'] = true; |
849 | $title = $this->titleFactory->newFromPageReference( $page ); |
850 | if ( !$title->isKnown() ) { |
851 | $vals['missing'] = true; |
852 | } |
853 | $pages[$fakeId] = $vals; |
854 | } |
855 | |
856 | // Output general page information for found titles |
857 | foreach ( $pageSet->getGoodPages() as $pageid => $page ) { |
858 | $vals = []; |
859 | $vals['pageid'] = $pageid; |
860 | $vals['ns'] = $page->getNamespace(); |
861 | $vals['title'] = $this->titleFormatter->getPrefixedText( $page ); |
862 | $pages[$pageid] = $vals; |
863 | } |
864 | |
865 | if ( count( $pages ) ) { |
866 | $pageSet->populateGeneratorData( $pages ); |
867 | ApiResult::setArrayType( $pages, 'BCarray' ); |
868 | |
869 | if ( $this->mParams['indexpageids'] ) { |
870 | $pageIDs = array_keys( ApiResult::stripMetadataNonRecursive( $pages ) ); |
871 | // json treats all map keys as strings - converting to match |
872 | $pageIDs = array_map( 'strval', $pageIDs ); |
873 | ApiResult::setIndexedTagName( $pageIDs, 'id' ); |
874 | $fit = $fit && $result->addValue( 'query', 'pageids', $pageIDs ); |
875 | } |
876 | |
877 | ApiResult::setIndexedTagName( $pages, 'page' ); |
878 | $fit = $fit && $result->addValue( 'query', 'pages', $pages ); |
879 | } |
880 | |
881 | if ( !$fit ) { |
882 | $this->dieWithError( 'apierror-badconfig-resulttoosmall', 'badconfig' ); |
883 | } |
884 | |
885 | if ( $this->mParams['export'] ) { |
886 | $this->doExport( $pageSet, $result ); |
887 | } |
888 | } |
889 | |
890 | /** |
891 | * @param ApiPageSet $pageSet Pages to be exported |
892 | * @param ApiResult $result Result to output to |
893 | */ |
894 | private function doExport( $pageSet, $result ) { |
895 | $exportTitles = []; |
896 | $titles = $pageSet->getGoodPages(); |
897 | if ( count( $titles ) ) { |
898 | /** @var Title $title */ |
899 | foreach ( $titles as $title ) { |
900 | if ( $this->getAuthority()->authorizeRead( 'read', $title ) ) { |
901 | $exportTitles[] = $title; |
902 | } |
903 | } |
904 | } |
905 | |
906 | $exporter = $this->wikiExporterFactory->getWikiExporter( $this->getDB() ); |
907 | $sink = new DumpStringOutput; |
908 | $exporter->setOutputSink( $sink ); |
909 | $exporter->setSchemaVersion( $this->mParams['exportschema'] ); |
910 | $exporter->openStream(); |
911 | foreach ( $exportTitles as $title ) { |
912 | $exporter->pageByTitle( $title ); |
913 | } |
914 | $exporter->closeStream(); |
915 | |
916 | // Don't check the size of exported stuff |
917 | // It's not continuable, so it would cause more |
918 | // problems than it'd solve |
919 | if ( $this->mParams['exportnowrap'] ) { |
920 | $result->reset(); |
921 | // Raw formatter will handle this |
922 | $result->addValue( null, 'text', $sink, ApiResult::NO_SIZE_CHECK ); |
923 | $result->addValue( null, 'mime', 'text/xml', ApiResult::NO_SIZE_CHECK ); |
924 | $result->addValue( null, 'filename', 'export.xml', ApiResult::NO_SIZE_CHECK ); |
925 | } else { |
926 | $result->addValue( 'query', 'export', $sink, ApiResult::NO_SIZE_CHECK ); |
927 | $result->addValue( 'query', ApiResult::META_BC_SUBELEMENTS, [ 'export' ] ); |
928 | } |
929 | } |
930 | |
931 | public function getAllowedParams( $flags = 0 ) { |
932 | $result = [ |
933 | 'prop' => [ |
934 | ParamValidator::PARAM_ISMULTI => true, |
935 | ParamValidator::PARAM_TYPE => 'submodule', |
936 | ], |
937 | 'list' => [ |
938 | ParamValidator::PARAM_ISMULTI => true, |
939 | ParamValidator::PARAM_TYPE => 'submodule', |
940 | ], |
941 | 'meta' => [ |
942 | ParamValidator::PARAM_ISMULTI => true, |
943 | ParamValidator::PARAM_TYPE => 'submodule', |
944 | ], |
945 | 'indexpageids' => false, |
946 | 'export' => false, |
947 | 'exportnowrap' => false, |
948 | 'exportschema' => [ |
949 | ParamValidator::PARAM_DEFAULT => WikiExporter::schemaVersion(), |
950 | ParamValidator::PARAM_TYPE => XmlDumpWriter::$supportedSchemas, |
951 | ], |
952 | 'iwurl' => false, |
953 | 'continue' => [ |
954 | ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', |
955 | ], |
956 | 'rawcontinue' => false, |
957 | ]; |
958 | if ( $flags ) { |
959 | $result += $this->getPageSet()->getFinalParams( $flags ); |
960 | } |
961 | |
962 | return $result; |
963 | } |
964 | |
965 | public function isReadMode() { |
966 | // We need to make an exception for certain meta modules that should be |
967 | // accessible even without the 'read' right. Restrict the exception as |
968 | // much as possible: no other modules allowed, and no pageset |
969 | // parameters either. We do allow the 'rawcontinue' and 'indexpageids' |
970 | // parameters since frameworks might add these unconditionally and they |
971 | // can't expose anything here. |
972 | $allowedParams = [ 'rawcontinue' => 1, 'indexpageids' => 1 ]; |
973 | $this->mParams = $this->extractRequestParams(); |
974 | $request = $this->getRequest(); |
975 | foreach ( $this->mParams + $this->getPageSet()->extractRequestParams() as $param => $value ) { |
976 | $needed = $param === 'meta'; |
977 | if ( !isset( $allowedParams[$param] ) && $request->getCheck( $param ) !== $needed ) { |
978 | return true; |
979 | } |
980 | } |
981 | |
982 | // Ask each module if it requires read mode. Any true => this returns |
983 | // true. |
984 | $modules = []; |
985 | $this->instantiateModules( $modules, 'meta' ); |
986 | foreach ( $modules as $module ) { |
987 | if ( $module->isReadMode() ) { |
988 | return true; |
989 | } |
990 | } |
991 | |
992 | return false; |
993 | } |
994 | |
995 | public function isWriteMode() { |
996 | // Ask each module if it requires write mode. If any require write mode this returns true. |
997 | $modules = []; |
998 | $this->mParams = $this->extractRequestParams(); |
999 | $this->instantiateModules( $modules, 'list' ); |
1000 | $this->instantiateModules( $modules, 'meta' ); |
1001 | $this->instantiateModules( $modules, 'prop' ); |
1002 | foreach ( $modules as $module ) { |
1003 | if ( $module->isWriteMode() ) { |
1004 | return true; |
1005 | } |
1006 | } |
1007 | |
1008 | return false; |
1009 | } |
1010 | |
1011 | protected function getExamplesMessages() { |
1012 | $title = Title::newMainPage()->getPrefixedText(); |
1013 | $mp = rawurlencode( $title ); |
1014 | |
1015 | return [ |
1016 | 'action=query&prop=revisions&meta=siteinfo&' . |
1017 | "titles={$mp}&rvprop=user|comment&continue=" |
1018 | => 'apihelp-query-example-revisions', |
1019 | 'action=query&generator=allpages&gapprefix=API/&prop=revisions&continue=' |
1020 | => 'apihelp-query-example-allpages', |
1021 | ]; |
1022 | } |
1023 | |
1024 | public function getHelpUrls() { |
1025 | return [ |
1026 | 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Query', |
1027 | 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Meta', |
1028 | 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Properties', |
1029 | 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Lists', |
1030 | ]; |
1031 | } |
1032 | } |