Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.15% covered (warning)
74.15%
175 / 236
35.71% covered (danger)
35.71%
5 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQuery
74.15% covered (warning)
74.15%
175 / 236
35.71% covered (danger)
35.71%
5 / 14
144.52
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
82.22% covered (warning)
82.22%
37 / 45
0.00% covered (danger)
0.00%
0 / 1
7.28
 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
67.19% covered (warning)
67.19%
43 / 64
0.00% covered (danger)
0.00%
0 / 1
44.35
 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 * 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
23use MediaWiki\Export\WikiExporterFactory;
24use MediaWiki\MainConfigNames;
25use MediaWiki\MediaWikiServices;
26use MediaWiki\Title\Title;
27use MediaWiki\Title\TitleFactory;
28use MediaWiki\Title\TitleFormatter;
29use Wikimedia\ObjectFactory\ObjectFactory;
30use 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 */
43class 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}