Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.89% covered (warning)
71.89%
179 / 249
35.71% covered (danger)
35.71%
5 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQuery
72.18% covered (warning)
72.18%
179 / 248
35.71% covered (danger)
35.71%
5 / 14
171.54
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
14 / 14
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.14% covered (warning)
74.14%
43 / 58
0.00% covered (danger)
0.00%
0 / 1
10.40
 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                'ChangeTagsFormatter',
459            ]
460        ],
461        'trackingcategories' => [
462            'class' => ApiQueryTrackingCategories::class,
463            'services' => [
464                'TrackingCategories',
465            ]
466        ],
467        'usercontribs' => [
468            'class' => ApiQueryUserContribs::class,
469            'services' => [
470                'CommentStore',
471                'UserIdentityLookup',
472                'UserNameUtils',
473                'RevisionStore',
474                'ChangeTagDefStore',
475                'ChangeTagsStore',
476                'ActorMigration',
477                'CommentFormatter',
478            ],
479        ],
480        'users' => [
481            'class' => ApiQueryUsers::class,
482            'services' => [
483                'UserNameUtils',
484                'UserFactory',
485                'UserGroupManager',
486                'GenderCache',
487                'AuthManager',
488                'TempUserConfig',
489                'TempUserDetailsLookup',
490            ],
491        ],
492        'watchlist' => [
493            'class' => ApiQueryWatchlist::class,
494            'services' => [
495                'CommentStore',
496                'ChangesListQueryFactory',
497                'RowCommentFormatter',
498                'TempUserConfig',
499                'LogFormatterFactory',
500                'RecentChangeLookup',
501                'TitleFormatter',
502                'WatchlistLabelStore',
503            ],
504        ],
505        'watchlistraw' => [
506            'class' => ApiQueryWatchlistRaw::class,
507            'services' => [
508                'WatchedItemQueryService',
509                'ContentLanguage',
510                'NamespaceInfo',
511                'GenderCache',
512            ]
513        ],
514    ];
515
516    /**
517     * List of Api Query meta modules
518     */
519    private const QUERY_META_MODULES = [
520        'allmessages' => [
521            'class' => ApiQueryAllMessages::class,
522            'services' => [
523                'ContentLanguage',
524                'LanguageFactory',
525                'LanguageNameUtils',
526                'LocalisationCache',
527                'MessageCache',
528            ]
529        ],
530        'authmanagerinfo' => [
531            'class' => ApiQueryAuthManagerInfo::class,
532            'services' => [
533                'AuthManager',
534            ]
535        ],
536        'siteinfo' => [
537            'class' => ApiQuerySiteinfo::class,
538            'services' => [
539                'UserOptionsLookup',
540                'UserGroupManager',
541                'HookContainer',
542                'LanguageConverterFactory',
543                'LanguageFactory',
544                'LanguageNameUtils',
545                'ContentLanguage',
546                'NamespaceInfo',
547                'InterwikiLookup',
548                'ParserFactory',
549                'MagicWordFactory',
550                'SpecialPageFactory',
551                'SkinFactory',
552                'DBLoadBalancer',
553                'ReadOnlyMode',
554                'UrlUtils',
555                'TempUserConfig',
556                'GroupPermissionsLookup',
557                'SBOMGenerator',
558                'UserIdentityUtils',
559            ]
560        ],
561        'userinfo' => [
562            'class' => ApiQueryUserInfo::class,
563            'services' => [
564                'TalkPageNotificationManager',
565                'WatchedItemStore',
566                'UserEditTracker',
567                'UserOptionsLookup',
568                'UserGroupManager',
569                'WatchlistLabelStore',
570            ]
571        ],
572        'filerepoinfo' => [
573            'class' => ApiQueryFileRepoInfo::class,
574            'services' => [
575                'RepoGroup',
576            ]
577        ],
578        'tokens' => [
579            'class' => ApiQueryTokens::class,
580        ],
581        'languageinfo' => [
582            'class' => ApiQueryLanguageinfo::class,
583            'services' => [
584                'LanguageFactory',
585                'LanguageNameUtils',
586                'LanguageFallback',
587                'LanguageConverterFactory',
588            ],
589        ],
590    ];
591
592    /**
593     * @var ApiPageSet
594     */
595    private $mPageSet;
596
597    /** @var array */
598    private $mParams;
599
600    private ApiModuleManager $mModuleMgr;
601
602    public function __construct(
603        ApiMain $main,
604        string $action,
605        ObjectFactory $objectFactory,
606        private readonly WikiExporterFactory $wikiExporterFactory,
607        private readonly TitleFormatter $titleFormatter,
608        private readonly TitleFactory $titleFactory,
609    ) {
610        parent::__construct( $main, $action );
611
612        $this->mModuleMgr = new ApiModuleManager(
613            $this,
614            $objectFactory
615        );
616
617        // Allow custom modules to be added in LocalSettings.php
618        $config = $this->getConfig();
619        $this->mModuleMgr->addModules( self::QUERY_PROP_MODULES, 'prop' );
620        $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIPropModules ), 'prop' );
621        $this->mModuleMgr->addModules( self::QUERY_LIST_MODULES, 'list' );
622        $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIListModules ), 'list' );
623        $this->mModuleMgr->addModules( self::QUERY_META_MODULES, 'meta' );
624        $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIMetaModules ), 'meta' );
625
626        $this->getHookRunner()->onApiQuery__moduleManager( $this->mModuleMgr );
627
628        // Create PageSet that will process titles/pageids/revids/generator
629        $this->mPageSet = new ApiPageSet( $this );
630    }
631
632    /**
633     * Overrides to return this instance's module manager.
634     * @return ApiModuleManager
635     */
636    public function getModuleManager() {
637        return $this->mModuleMgr;
638    }
639
640    /**
641     * Gets the set of pages the user has requested (or generated)
642     * @return ApiPageSet
643     */
644    public function getPageSet() {
645        return $this->mPageSet;
646    }
647
648    /**
649     * @return ApiFormatRaw|null
650     */
651    public function getCustomPrinter() {
652        // If &exportnowrap is set, use the raw formatter
653        if ( $this->getParameter( 'export' ) &&
654            $this->getParameter( 'exportnowrap' )
655        ) {
656            return new ApiFormatRaw( $this->getMain(),
657                $this->getMain()->createPrinterByName( 'xml' ) );
658        } else {
659            return null;
660        }
661    }
662
663    /**
664     * Query execution happens in the following steps:
665     * #1 Create a PageSet object with any pages requested by the user
666     * #2 If using a generator, execute it to get a new ApiPageSet object
667     * #3 Instantiate all requested modules.
668     *    This way the PageSet object will know what shared data is required,
669     *    and minimize DB calls.
670     * #4 Output all normalization and redirect resolution information
671     * #5 Execute all requested modules
672     */
673    public function execute() {
674        $this->mParams = $this->extractRequestParams();
675
676        // Instantiate requested modules
677        $allModules = [];
678        $this->instantiateModules( $allModules, 'prop' );
679        $propModules = array_keys( $allModules );
680        $this->instantiateModules( $allModules, 'list' );
681        $this->instantiateModules( $allModules, 'meta' );
682
683        // Filter modules based on continue parameter
684        $continuationManager = new ApiContinuationManager( $this, $allModules, $propModules );
685        $this->setContinuationManager( $continuationManager );
686        /** @var ApiQueryBase[] $modules */
687        $modules = $continuationManager->getRunModules();
688        '@phan-var ApiQueryBase[] $modules';
689
690        // Allow extensions to stop execution for arbitrary reasons.
691        $message = 'hookaborted';
692        if ( !$this->getHookRunner()->onApiQueryCheckCanExecute( $modules, $this->getUser(), $message ) ) {
693            $this->dieWithError( $message );
694        }
695
696        $statsFactory = MediaWikiServices::getInstance()->getStatsFactory();
697
698        if ( !$continuationManager->isGeneratorDone() ) {
699            // Query modules may optimize data requests through the $this->getPageSet()
700            // object by adding extra fields from the page table.
701            foreach ( $modules as $module ) {
702                // Augment api-query.$module.executeTiming metric with timings for requestExtraData()
703                $timer = $statsFactory->getTiming( 'api_query_extraDataTiming_seconds' )
704                    ->setLabel( 'module', $module->getModuleName() )
705                    ->start();
706                $module->requestExtraData( $this->mPageSet );
707                $timer->stop();
708            }
709            // Populate page/revision information
710            $this->mPageSet->execute();
711            // Record page information (title, namespace, if exists, etc)
712            $this->outputGeneralPageInfo();
713        } else {
714            $this->mPageSet->executeDryRun();
715        }
716
717        $cacheMode = $this->mPageSet->getCacheMode();
718
719        // Execute all unfinished modules
720        foreach ( $modules as $module ) {
721            // Break down of the api.query.executeTiming metric by query module.
722            $timer = $statsFactory->getTiming( 'api_query_executeTiming_seconds' )
723                ->setLabel( 'module', $module->getModuleName() )
724                ->start();
725
726            $params = $module->extractRequestParams();
727            $cacheMode = $this->mergeCacheMode(
728                $cacheMode, $module->getCacheMode( $params ) );
729            $scope = LoggerFactory::getContext()->addScoped( [
730                'context.api_query_module_name' => $module->getModuleName(),
731            ] );
732
733            // Wrap the execution in a try/catch to record metrics for success and errors
734            try {
735                $module->execute();
736                $module->recordUnifiedMetrics();
737            } catch ( \Throwable $e ) {
738                // Unified metrics for errors
739                $module->recordUnifiedMetrics(
740                    [
741                        'status' => 'error_' . $e->getCode(), // Failure codes
742                    ]
743                );
744                // Re-throw the exception so it's bubbled up
745                throw $e;
746            }
747            ScopedCallback::consume( $scope );
748
749            $timer->stop();
750
751            $this->getHookRunner()->onAPIQueryAfterExecute( $module );
752        }
753
754        // Set the cache mode
755        $this->getMain()->setCacheMode( $cacheMode );
756
757        // Write the continuation data into the result
758        $this->setContinuationManager( null );
759        if ( $this->mParams['rawcontinue'] ) {
760            $data = $continuationManager->getRawNonContinuation();
761            if ( $data ) {
762                $this->getResult()->addValue( null, 'query-noncontinue', $data,
763                    ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
764            }
765            $data = $continuationManager->getRawContinuation();
766            if ( $data ) {
767                $this->getResult()->addValue( null, 'query-continue', $data,
768                    ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
769            }
770        } else {
771            $continuationManager->setContinuationIntoResult( $this->getResult() );
772        }
773    }
774
775    /**
776     * Update a cache mode string, applying the cache mode of a new module to it.
777     * The cache mode may increase in the level of privacy, but public modules
778     * added to private data do not decrease the level of privacy.
779     *
780     * @param string $cacheMode
781     * @param string $modCacheMode
782     * @return string
783     */
784    protected function mergeCacheMode( $cacheMode, $modCacheMode ) {
785        if ( $modCacheMode === 'anon-public-user-private' ) {
786            if ( $cacheMode !== 'private' ) {
787                $cacheMode = 'anon-public-user-private';
788            }
789        } elseif ( $modCacheMode === 'public' ) {
790            // do nothing, if it's public already it will stay public
791        } else {
792            $cacheMode = 'private';
793        }
794
795        return $cacheMode;
796    }
797
798    /**
799     * Create instances of all modules requested by the client
800     * @param array &$modules To append instantiated modules to
801     * @param string $param Parameter name to read modules from
802     */
803    private function instantiateModules( &$modules, $param ) {
804        $wasPosted = $this->getRequest()->wasPosted();
805        if ( isset( $this->mParams[$param] ) ) {
806            foreach ( $this->mParams[$param] as $moduleName ) {
807                $instance = $this->mModuleMgr->getModule( $moduleName, $param );
808                if ( $instance === null ) {
809                    ApiBase::dieDebug( __METHOD__, 'Error instantiating module' );
810                }
811                if ( !$wasPosted && $instance->mustBePosted() ) {
812                    $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $moduleName ] );
813                }
814                // Ignore duplicates. TODO 2.0: die()?
815                if ( !array_key_exists( $moduleName, $modules ) ) {
816                    $modules[$moduleName] = $instance;
817                }
818            }
819        }
820    }
821
822    /**
823     * Appends an element for each page in the current pageSet with the
824     * most general information (id, title), plus any title normalizations
825     * and missing or invalid title/pageids/revids.
826     */
827    private function outputGeneralPageInfo() {
828        $pageSet = $this->getPageSet();
829        $result = $this->getResult();
830
831        // We can't really handle max-result-size failure here, but we need to
832        // check anyway in case someone set the limit stupidly low.
833        $fit = true;
834
835        $values = $pageSet->getNormalizedTitlesAsResult( $result );
836        if ( $values ) {
837            // @phan-suppress-next-line PhanRedundantCondition
838            $fit = $fit && $result->addValue( 'query', 'normalized', $values );
839        }
840        $values = $pageSet->getConvertedTitlesAsResult( $result );
841        if ( $values ) {
842            $fit = $fit && $result->addValue( 'query', 'converted', $values );
843        }
844        $values = $pageSet->getInterwikiTitlesAsResult( $result, $this->mParams['iwurl'] );
845        if ( $values ) {
846            $fit = $fit && $result->addValue( 'query', 'interwiki', $values );
847        }
848        $values = $pageSet->getRedirectTitlesAsResult( $result );
849        if ( $values ) {
850            $fit = $fit && $result->addValue( 'query', 'redirects', $values );
851        }
852        $values = $pageSet->getMissingRevisionIDsAsResult( $result );
853        if ( $values ) {
854            $fit = $fit && $result->addValue( 'query', 'badrevids', $values );
855        }
856
857        // Page elements
858        // Cannot use ApiPageSet::getInvalidTitlesAndRevisions, it does not set $fakeId
859        $pages = [];
860
861        // Report any missing titles
862        foreach ( $pageSet->getMissingPages() as $fakeId => $page ) {
863            $vals = [
864                'ns' => $page->getNamespace(),
865                'title' => $this->titleFormatter->getPrefixedText( $page ),
866                'missing' => true,
867            ];
868            $title = $this->titleFactory->newFromPageIdentity( $page );
869            if ( $title->isKnown() ) {
870                $vals['known'] = true;
871            }
872            $pages[$fakeId] = $vals;
873        }
874        // Report any invalid titles
875        foreach ( $pageSet->getInvalidTitlesAndReasons() as $fakeId => $data ) {
876            $pages[$fakeId] = $data + [ 'invalid' => true ];
877        }
878        // Report any missing page ids
879        foreach ( $pageSet->getMissingPageIDs() as $pageid ) {
880            $pages[$pageid] = [
881                'pageid' => $pageid,
882                'missing' => true,
883            ];
884        }
885        // Report special pages
886        /** @var \MediaWiki\Page\PageReference $page */
887        foreach ( $pageSet->getSpecialPages() as $fakeId => $page ) {
888            $vals = [
889                'ns' => $page->getNamespace(),
890                'title' => $this->titleFormatter->getPrefixedText( $page ),
891                'special' => true,
892            ];
893            $title = $this->titleFactory->newFromPageReference( $page );
894            if ( !$title->isKnown() ) {
895                $vals['missing'] = true;
896            }
897            $pages[$fakeId] = $vals;
898        }
899
900        // Output general page information for found titles
901        foreach ( $pageSet->getGoodPages() as $pageid => $page ) {
902            $pages[$pageid] = [
903                'pageid' => $pageid,
904                'ns' => $page->getNamespace(),
905                'title' => $this->titleFormatter->getPrefixedText( $page ),
906            ];
907        }
908
909        if ( count( $pages ) ) {
910            $pageSet->populateGeneratorData( $pages );
911            ApiResult::setArrayType( $pages, 'BCarray' );
912
913            if ( $this->mParams['indexpageids'] ) {
914                $pageIDs = array_keys( ApiResult::stripMetadataNonRecursive( $pages ) );
915                // json treats all map keys as strings - converting to match
916                $pageIDs = array_map( 'strval', $pageIDs );
917                ApiResult::setIndexedTagName( $pageIDs, 'id' );
918                $fit = $fit && $result->addValue( 'query', 'pageids', $pageIDs );
919            }
920
921            ApiResult::setIndexedTagName( $pages, 'page' );
922            $fit = $fit && $result->addValue( 'query', 'pages', $pages );
923        }
924
925        if ( !$fit ) {
926            $this->dieWithError( 'apierror-badconfig-resulttoosmall', 'badconfig' );
927        }
928
929        if ( $this->mParams['export'] ) {
930            $this->doExport( $pageSet, $result );
931        }
932    }
933
934    /**
935     * @param ApiPageSet $pageSet Pages to be exported
936     * @param ApiResult $result Result to output to
937     */
938    private function doExport( $pageSet, $result ) {
939        $exportTitles = [];
940        $titles = $pageSet->getGoodPages();
941        if ( count( $titles ) ) {
942            /** @var Title $title */
943            foreach ( $titles as $title ) {
944                if ( $this->getAuthority()->authorizeRead( 'read', $title ) ) {
945                    $exportTitles[] = $title;
946                }
947            }
948        }
949
950        $exporter = $this->wikiExporterFactory->getWikiExporter( $this->getDB() );
951        $sink = new DumpStringOutput;
952        $exporter->setOutputSink( $sink );
953        $exporter->setSchemaVersion( $this->mParams['exportschema'] );
954        $exporter->openStream();
955        foreach ( $exportTitles as $title ) {
956            $exporter->pageByTitle( $title );
957        }
958        $exporter->closeStream();
959
960        // Don't check the size of exported stuff
961        // It's not continuable, so it would cause more
962        // problems than it'd solve
963        if ( $this->mParams['exportnowrap'] ) {
964            $result->reset();
965            // Raw formatter will handle this
966            $result->addValue( null, 'text', $sink, ApiResult::NO_SIZE_CHECK );
967            $result->addValue( null, 'mime', 'text/xml', ApiResult::NO_SIZE_CHECK );
968            $result->addValue( null, 'filename', 'export.xml', ApiResult::NO_SIZE_CHECK );
969        } else {
970            $result->addValue( 'query', 'export', $sink, ApiResult::NO_SIZE_CHECK );
971            $result->addValue( 'query', ApiResult::META_BC_SUBELEMENTS, [ 'export' ] );
972        }
973    }
974
975    /** @inheritDoc */
976    public function getAllowedParams( $flags = 0 ) {
977        $result = [
978            'prop' => [
979                ParamValidator::PARAM_ISMULTI => true,
980                ParamValidator::PARAM_TYPE => 'submodule',
981            ],
982            'list' => [
983                ParamValidator::PARAM_ISMULTI => true,
984                ParamValidator::PARAM_TYPE => 'submodule',
985            ],
986            'meta' => [
987                ParamValidator::PARAM_ISMULTI => true,
988                ParamValidator::PARAM_TYPE => 'submodule',
989            ],
990            'indexpageids' => false,
991            'export' => false,
992            'exportnowrap' => false,
993            'exportschema' => [
994                ParamValidator::PARAM_DEFAULT => WikiExporter::schemaVersion(),
995                ParamValidator::PARAM_TYPE => XmlDumpWriter::$supportedSchemas,
996            ],
997            'iwurl' => false,
998            'continue' => [
999                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
1000            ],
1001            'rawcontinue' => false,
1002        ];
1003        if ( $flags ) {
1004            $result += $this->getPageSet()->getFinalParams( $flags );
1005        }
1006
1007        return $result;
1008    }
1009
1010    /** @inheritDoc */
1011    public function isReadMode() {
1012        // We need to make an exception for certain meta modules that should be
1013        // accessible even without the 'read' right. Restrict the exception as
1014        // much as possible: no other modules allowed, and no pageset
1015        // parameters either. We do allow the 'rawcontinue' and 'indexpageids'
1016        // parameters since frameworks might add these unconditionally and they
1017        // can't expose anything here.
1018        $allowedParams = [ 'rawcontinue' => 1, 'indexpageids' => 1 ];
1019        $this->mParams = $this->extractRequestParams();
1020        $request = $this->getRequest();
1021        foreach ( $this->mParams + $this->getPageSet()->extractRequestParams() as $param => $value ) {
1022            $needed = $param === 'meta';
1023            if ( !isset( $allowedParams[$param] ) && $request->getCheck( $param ) !== $needed ) {
1024                return true;
1025            }
1026        }
1027
1028        // Ask each module if it requires read mode. Any true => this returns
1029        // true.
1030        $modules = [];
1031        $this->instantiateModules( $modules, 'meta' );
1032        foreach ( $modules as $module ) {
1033            if ( $module->isReadMode() ) {
1034                return true;
1035            }
1036        }
1037
1038        return false;
1039    }
1040
1041    /** @inheritDoc */
1042    public function isWriteMode() {
1043        // Ask each module if it requires write mode. If any require write mode this returns true.
1044        $modules = [];
1045        $this->mParams = $this->extractRequestParams();
1046        $this->instantiateModules( $modules, 'list' );
1047        $this->instantiateModules( $modules, 'meta' );
1048        $this->instantiateModules( $modules, 'prop' );
1049        foreach ( $modules as $module ) {
1050            if ( $module->isWriteMode() ) {
1051                return true;
1052            }
1053        }
1054
1055        return false;
1056    }
1057
1058    /** @inheritDoc */
1059    protected function getExamplesMessages() {
1060        $title = Title::newMainPage()->getPrefixedText();
1061        $mp = rawurlencode( $title );
1062
1063        return [
1064            'action=query&prop=revisions&meta=siteinfo&' .
1065                "titles={$mp}&rvprop=user|comment&continue="
1066                => 'apihelp-query-example-revisions',
1067            'action=query&generator=allpages&gapprefix=API/&prop=revisions&continue='
1068                => 'apihelp-query-example-allpages',
1069        ];
1070    }
1071
1072    /** @inheritDoc */
1073    public function getHelpUrls() {
1074        return [
1075            'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Query',
1076            'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Meta',
1077            'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Properties',
1078            'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Lists',
1079        ];
1080    }
1081}
1082
1083/** @deprecated class alias since 1.43 */
1084class_alias( ApiQuery::class, 'ApiQuery' );