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