3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
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.
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.
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
23 namespace MediaWiki\Api
;
26 use MediaWiki\Export\WikiExporterFactory
;
27 use MediaWiki\MainConfigNames
;
28 use MediaWiki\MediaWikiServices
;
29 use MediaWiki\Title\Title
;
30 use MediaWiki\Title\TitleFactory
;
31 use MediaWiki\Title\TitleFormatter
;
33 use Wikimedia\ObjectFactory\ObjectFactory
;
34 use Wikimedia\ParamValidator\ParamValidator
;
38 * This is the main query class. It behaves similar to ApiMain: based on the
39 * parameters given, it will create a list of titles to work on (an ApiPageSet
40 * object), instantiate and execute various property/list/meta modules, and
41 * assemble all resulting data into a single ApiResult object.
43 * In generator mode, a generator will be executed first to populate a second
44 * ApiPageSet object, and that object will be used for all subsequent modules.
48 class ApiQuery
extends ApiBase
{
51 * List of Api Query prop modules
53 private const QUERY_PROP_MODULES
= [
55 'class' => ApiQueryCategories
::class,
58 'class' => ApiQueryCategoryInfo
::class,
61 'class' => ApiQueryContributors
::class,
66 'GroupPermissionsLookup',
70 'deletedrevisions' => [
71 'class' => ApiQueryDeletedRevisions
::class,
74 'ContentHandlerFactory',
88 'class' => ApiQueryDuplicateFiles
::class,
94 'class' => ApiQueryExternalLinks
::class,
100 'class' => ApiQueryBacklinksprop
::class,
102 // Same as for linkshere, redirects, transcludedin
107 'class' => ApiQueryImages
::class,
110 'class' => ApiQueryImageInfo
::class,
112 // Same as for stashimageinfo
119 'class' => ApiQueryInfo
::class,
127 'LanguageConverterFactory',
132 'IntroMessageBuilder',
133 'PreloadedContentBuilder',
139 'class' => ApiQueryLinks
::class,
141 // Same as for templates
147 'class' => ApiQueryBacklinksprop
::class,
149 // Same as for fileusage, redirects, transcludedin
154 'class' => ApiQueryIWLinks
::class,
160 'class' => ApiQueryLangLinks
::class,
168 'class' => ApiQueryPageProps
::class,
174 'class' => ApiQueryBacklinksprop
::class,
176 // Same as for fileusage, linkshere, transcludedin
181 'class' => ApiQueryRevisions
::class,
184 'ContentHandlerFactory',
191 'ContentTransformer',
198 'stashimageinfo' => [
199 'class' => ApiQueryStashImageInfo
::class,
201 // Same as for imageinfo
208 'class' => ApiQueryLinks
::class,
216 'class' => ApiQueryBacklinksprop
::class,
218 // Same as for fileusage, linkshere, redirects
225 * List of Api Query list modules
227 private const QUERY_LIST_MODULES
= [
229 'class' => ApiQueryAllCategories
::class,
231 'alldeletedrevisions' => [
232 'class' => ApiQueryAllDeletedRevisions
::class,
235 'ContentHandlerFactory',
242 'ContentTransformer',
249 'class' => ApiQueryAllLinks
::class,
251 // Same as for alllinks, allredirects, alltransclusions
258 'class' => ApiQueryAllImages
::class,
261 'GroupPermissionsLookup',
265 'class' => ApiQueryAllLinks
::class,
267 // Same as for allfileusages, allredirects, alltransclusions
274 'class' => ApiQueryAllPages
::class,
282 'class' => ApiQueryAllLinks
::class,
284 // Same as for allfileusages, alllinks, alltransclusions
291 'class' => ApiQueryAllRevisions
::class,
294 'ContentHandlerFactory',
301 'ContentTransformer',
307 'mystashedfiles' => [
308 'class' => ApiQueryMyStashedFiles
::class,
310 'alltransclusions' => [
311 'class' => ApiQueryAllLinks
::class,
313 // Same as for allfileusages, alllinks, allredirects
320 'class' => ApiQueryAllUsers
::class,
324 'GroupPermissionsLookup',
330 'class' => ApiQueryBacklinks
::class,
336 'class' => ApiQueryBlocks
::class,
338 'DatabaseBlockStore',
340 'BlockRestrictionStore',
346 'categorymembers' => [
347 'class' => ApiQueryCategoryMembers
::class,
353 'class' => ApiQueryDeletedrevs
::class,
356 'RowCommentFormatter',
364 'class' => ApiQueryBacklinks
::class,
370 'class' => ApiQueryExtLinksUsage
::class,
376 'class' => ApiQueryFilearchive
::class,
383 'class' => ApiQueryBacklinks
::class,
389 'class' => ApiQueryIWBacklinks
::class,
392 'class' => ApiQueryLangBacklinks
::class,
395 'class' => ApiQueryLogEvents
::class,
398 'RowCommentFormatter',
402 'LogFormatterFactory',
406 'class' => ApiQueryPagesWithProp
::class,
409 'class' => ApiQueryPagePropNames
::class,
412 'class' => ApiQueryPrefixSearch
::class,
414 'SearchEngineConfig',
415 'SearchEngineFactory',
418 'protectedtitles' => [
419 'class' => ApiQueryProtectedTitles
::class,
422 'RowCommentFormatter'
426 'class' => ApiQueryQueryPage
::class,
428 'SpecialPageFactory',
432 'class' => ApiQueryRandom
::class,
435 'class' => ApiQueryRecentChanges
::class,
438 'RowCommentFormatter',
445 'LogFormatterFactory',
449 'class' => ApiQuerySearch
::class,
451 'SearchEngineConfig',
452 'SearchEngineFactory',
457 'class' => ApiQueryTags
::class,
463 'class' => ApiQueryUserContribs
::class,
466 'UserIdentityLookup',
476 'class' => ApiQueryUsers
::class,
486 'class' => ApiQueryWatchlist
::class,
489 'WatchedItemQueryService',
495 'LogFormatterFactory',
499 'class' => ApiQueryWatchlistRaw
::class,
501 'WatchedItemQueryService',
510 * List of Api Query meta modules
512 private const QUERY_META_MODULES
= [
514 'class' => ApiQueryAllMessages
::class,
523 'authmanagerinfo' => [
524 'class' => ApiQueryAuthManagerInfo
::class,
530 'class' => ApiQuerySiteinfo
::class,
535 'LanguageConverterFactory',
543 'SpecialPageFactory',
552 'class' => ApiQueryUserInfo
::class,
554 'TalkPageNotificationManager',
562 'class' => ApiQueryFileRepoInfo
::class,
568 'class' => ApiQueryTokens
::class,
571 'class' => ApiQueryLanguageinfo
::class,
576 'LanguageConverterFactory',
588 /** @var ApiModuleManager */
591 private WikiExporterFactory
$wikiExporterFactory;
592 private TitleFormatter
$titleFormatter;
593 private TitleFactory
$titleFactory;
595 public function __construct(
598 ObjectFactory
$objectFactory,
599 WikiExporterFactory
$wikiExporterFactory,
600 TitleFormatter
$titleFormatter,
601 TitleFactory
$titleFactory
603 parent
::__construct( $main, $action );
605 $this->mModuleMgr
= new ApiModuleManager(
610 // Allow custom modules to be added in LocalSettings.php
611 $config = $this->getConfig();
612 $this->mModuleMgr
->addModules( self
::QUERY_PROP_MODULES
, 'prop' );
613 $this->mModuleMgr
->addModules( $config->get( MainConfigNames
::APIPropModules
), 'prop' );
614 $this->mModuleMgr
->addModules( self
::QUERY_LIST_MODULES
, 'list' );
615 $this->mModuleMgr
->addModules( $config->get( MainConfigNames
::APIListModules
), 'list' );
616 $this->mModuleMgr
->addModules( self
::QUERY_META_MODULES
, 'meta' );
617 $this->mModuleMgr
->addModules( $config->get( MainConfigNames
::APIMetaModules
), 'meta' );
619 $this->getHookRunner()->onApiQuery__moduleManager( $this->mModuleMgr
);
621 // Create PageSet that will process titles/pageids/revids/generator
622 $this->mPageSet
= new ApiPageSet( $this );
623 $this->wikiExporterFactory
= $wikiExporterFactory;
624 $this->titleFormatter
= $titleFormatter;
625 $this->titleFactory
= $titleFactory;
629 * Overrides to return this instance's module manager.
630 * @return ApiModuleManager
632 public function getModuleManager() {
633 return $this->mModuleMgr
;
637 * Gets the set of pages the user has requested (or generated)
640 public function getPageSet() {
641 return $this->mPageSet
;
645 * @return ApiFormatRaw|null
647 public function getCustomPrinter() {
648 // If &exportnowrap is set, use the raw formatter
649 if ( $this->getParameter( 'export' ) &&
650 $this->getParameter( 'exportnowrap' )
652 return new ApiFormatRaw( $this->getMain(),
653 $this->getMain()->createPrinterByName( 'xml' ) );
660 * Query execution happens in the following steps:
661 * #1 Create a PageSet object with any pages requested by the user
662 * #2 If using a generator, execute it to get a new ApiPageSet object
663 * #3 Instantiate all requested modules.
664 * This way the PageSet object will know what shared data is required,
665 * and minimize DB calls.
666 * #4 Output all normalization and redirect resolution information
667 * #5 Execute all requested modules
669 public function execute() {
670 $this->mParams
= $this->extractRequestParams();
672 // Instantiate requested modules
674 $this->instantiateModules( $allModules, 'prop' );
675 $propModules = array_keys( $allModules );
676 $this->instantiateModules( $allModules, 'list' );
677 $this->instantiateModules( $allModules, 'meta' );
679 // Filter modules based on continue parameter
680 $continuationManager = new ApiContinuationManager( $this, $allModules, $propModules );
681 $this->setContinuationManager( $continuationManager );
682 /** @var ApiQueryBase[] $modules */
683 $modules = $continuationManager->getRunModules();
684 '@phan-var ApiQueryBase[] $modules';
686 // Allow extensions to stop execution for arbitrary reasons.
687 $message = 'hookaborted';
688 if ( !$this->getHookRunner()->onApiQueryCheckCanExecute( $modules, $this->getUser(), $message ) ) {
689 $this->dieWithError( $message );
692 $statsFactory = MediaWikiServices
::getInstance()->getStatsFactory();
694 if ( !$continuationManager->isGeneratorDone() ) {
695 // Query modules may optimize data requests through the $this->getPageSet()
696 // object by adding extra fields from the page table.
697 foreach ( $modules as $module ) {
698 // Augment api-query.$module.executeTiming metric with timings for requestExtraData()
699 $timer = $statsFactory->getTiming( 'api_query_extraDataTiming_seconds' )
700 ->setLabel( 'module', $module->getModuleName() )
701 ->copyToStatsdAt( 'api-query.' . $module->getModuleName() . '.extraDataTiming' );
703 $module->requestExtraData( $this->mPageSet
);
706 // Populate page/revision information
707 $this->mPageSet
->execute();
708 // Record page information (title, namespace, if exists, etc)
709 $this->outputGeneralPageInfo();
711 $this->mPageSet
->executeDryRun();
714 $cacheMode = $this->mPageSet
->getCacheMode();
716 // Execute all unfinished modules
717 foreach ( $modules as $module ) {
718 // Break down of the api.query.executeTiming metric by query module.
719 $timer = $statsFactory->getTiming( 'api_query_executeTiming_seconds' )
720 ->setLabel( 'module', $module->getModuleName() )
721 ->copyToStatsdAt( 'api-query.' . $module->getModuleName() . '.executeTiming' );
724 $params = $module->extractRequestParams();
725 $cacheMode = $this->mergeCacheMode(
726 $cacheMode, $module->getCacheMode( $params ) );
731 $this->getHookRunner()->onAPIQueryAfterExecute( $module );
734 // Set the cache mode
735 $this->getMain()->setCacheMode( $cacheMode );
737 // Write the continuation data into the result
738 $this->setContinuationManager( null );
739 if ( $this->mParams
['rawcontinue'] ) {
740 $data = $continuationManager->getRawNonContinuation();
742 $this->getResult()->addValue( null, 'query-noncontinue', $data,
743 ApiResult
::ADD_ON_TOP | ApiResult
::NO_SIZE_CHECK
);
745 $data = $continuationManager->getRawContinuation();
747 $this->getResult()->addValue( null, 'query-continue', $data,
748 ApiResult
::ADD_ON_TOP | ApiResult
::NO_SIZE_CHECK
);
751 $continuationManager->setContinuationIntoResult( $this->getResult() );
756 * Update a cache mode string, applying the cache mode of a new module to it.
757 * The cache mode may increase in the level of privacy, but public modules
758 * added to private data do not decrease the level of privacy.
760 * @param string $cacheMode
761 * @param string $modCacheMode
764 protected function mergeCacheMode( $cacheMode, $modCacheMode ) {
765 if ( $modCacheMode === 'anon-public-user-private' ) {
766 if ( $cacheMode !== 'private' ) {
767 $cacheMode = 'anon-public-user-private';
769 } elseif ( $modCacheMode === 'public' ) {
770 // do nothing, if it's public already it will stay public
772 $cacheMode = 'private';
779 * Create instances of all modules requested by the client
780 * @param array &$modules To append instantiated modules to
781 * @param string $param Parameter name to read modules from
783 private function instantiateModules( &$modules, $param ) {
784 $wasPosted = $this->getRequest()->wasPosted();
785 if ( isset( $this->mParams
[$param] ) ) {
786 foreach ( $this->mParams
[$param] as $moduleName ) {
787 $instance = $this->mModuleMgr
->getModule( $moduleName, $param );
788 if ( $instance === null ) {
789 ApiBase
::dieDebug( __METHOD__
, 'Error instantiating module' );
791 if ( !$wasPosted && $instance->mustBePosted() ) {
792 $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $moduleName ] );
794 // Ignore duplicates. TODO 2.0: die()?
795 if ( !array_key_exists( $moduleName, $modules ) ) {
796 $modules[$moduleName] = $instance;
803 * Appends an element for each page in the current pageSet with the
804 * most general information (id, title), plus any title normalizations
805 * and missing or invalid title/pageids/revids.
807 private function outputGeneralPageInfo() {
808 $pageSet = $this->getPageSet();
809 $result = $this->getResult();
811 // We can't really handle max-result-size failure here, but we need to
812 // check anyway in case someone set the limit stupidly low.
815 $values = $pageSet->getNormalizedTitlesAsResult( $result );
817 // @phan-suppress-next-line PhanRedundantCondition
818 $fit = $fit && $result->addValue( 'query', 'normalized', $values );
820 $values = $pageSet->getConvertedTitlesAsResult( $result );
822 $fit = $fit && $result->addValue( 'query', 'converted', $values );
824 $values = $pageSet->getInterwikiTitlesAsResult( $result, $this->mParams
['iwurl'] );
826 $fit = $fit && $result->addValue( 'query', 'interwiki', $values );
828 $values = $pageSet->getRedirectTitlesAsResult( $result );
830 $fit = $fit && $result->addValue( 'query', 'redirects', $values );
832 $values = $pageSet->getMissingRevisionIDsAsResult( $result );
834 $fit = $fit && $result->addValue( 'query', 'badrevids', $values );
838 // Cannot use ApiPageSet::getInvalidTitlesAndRevisions, it does not set $fakeId
841 // Report any missing titles
842 foreach ( $pageSet->getMissingPages() as $fakeId => $page ) {
844 $vals['ns'] = $page->getNamespace();
845 $vals['title'] = $this->titleFormatter
->getPrefixedText( $page );
846 $vals['missing'] = true;
847 $title = $this->titleFactory
->newFromPageIdentity( $page );
848 if ( $title->isKnown() ) {
849 $vals['known'] = true;
851 $pages[$fakeId] = $vals;
853 // Report any invalid titles
854 foreach ( $pageSet->getInvalidTitlesAndReasons() as $fakeId => $data ) {
855 $pages[$fakeId] = $data +
[ 'invalid' => true ];
857 // Report any missing page ids
858 foreach ( $pageSet->getMissingPageIDs() as $pageid ) {
864 // Report special pages
865 /** @var \MediaWiki\Page\PageReference $page */
866 foreach ( $pageSet->getSpecialPages() as $fakeId => $page ) {
868 $vals['ns'] = $page->getNamespace();
869 $vals['title'] = $this->titleFormatter
->getPrefixedText( $page );
870 $vals['special'] = true;
871 $title = $this->titleFactory
->newFromPageReference( $page );
872 if ( !$title->isKnown() ) {
873 $vals['missing'] = true;
875 $pages[$fakeId] = $vals;
878 // Output general page information for found titles
879 foreach ( $pageSet->getGoodPages() as $pageid => $page ) {
881 $vals['pageid'] = $pageid;
882 $vals['ns'] = $page->getNamespace();
883 $vals['title'] = $this->titleFormatter
->getPrefixedText( $page );
884 $pages[$pageid] = $vals;
887 if ( count( $pages ) ) {
888 $pageSet->populateGeneratorData( $pages );
889 ApiResult
::setArrayType( $pages, 'BCarray' );
891 if ( $this->mParams
['indexpageids'] ) {
892 $pageIDs = array_keys( ApiResult
::stripMetadataNonRecursive( $pages ) );
893 // json treats all map keys as strings - converting to match
894 $pageIDs = array_map( 'strval', $pageIDs );
895 ApiResult
::setIndexedTagName( $pageIDs, 'id' );
896 $fit = $fit && $result->addValue( 'query', 'pageids', $pageIDs );
899 ApiResult
::setIndexedTagName( $pages, 'page' );
900 $fit = $fit && $result->addValue( 'query', 'pages', $pages );
904 $this->dieWithError( 'apierror-badconfig-resulttoosmall', 'badconfig' );
907 if ( $this->mParams
['export'] ) {
908 $this->doExport( $pageSet, $result );
913 * @param ApiPageSet $pageSet Pages to be exported
914 * @param ApiResult $result Result to output to
916 private function doExport( $pageSet, $result ) {
918 $titles = $pageSet->getGoodPages();
919 if ( count( $titles ) ) {
920 /** @var Title $title */
921 foreach ( $titles as $title ) {
922 if ( $this->getAuthority()->authorizeRead( 'read', $title ) ) {
923 $exportTitles[] = $title;
928 $exporter = $this->wikiExporterFactory
->getWikiExporter( $this->getDB() );
929 $sink = new DumpStringOutput
;
930 $exporter->setOutputSink( $sink );
931 $exporter->setSchemaVersion( $this->mParams
['exportschema'] );
932 $exporter->openStream();
933 foreach ( $exportTitles as $title ) {
934 $exporter->pageByTitle( $title );
936 $exporter->closeStream();
938 // Don't check the size of exported stuff
939 // It's not continuable, so it would cause more
940 // problems than it'd solve
941 if ( $this->mParams
['exportnowrap'] ) {
943 // Raw formatter will handle this
944 $result->addValue( null, 'text', $sink, ApiResult
::NO_SIZE_CHECK
);
945 $result->addValue( null, 'mime', 'text/xml', ApiResult
::NO_SIZE_CHECK
);
946 $result->addValue( null, 'filename', 'export.xml', ApiResult
::NO_SIZE_CHECK
);
948 $result->addValue( 'query', 'export', $sink, ApiResult
::NO_SIZE_CHECK
);
949 $result->addValue( 'query', ApiResult
::META_BC_SUBELEMENTS
, [ 'export' ] );
953 public function getAllowedParams( $flags = 0 ) {
956 ParamValidator
::PARAM_ISMULTI
=> true,
957 ParamValidator
::PARAM_TYPE
=> 'submodule',
960 ParamValidator
::PARAM_ISMULTI
=> true,
961 ParamValidator
::PARAM_TYPE
=> 'submodule',
964 ParamValidator
::PARAM_ISMULTI
=> true,
965 ParamValidator
::PARAM_TYPE
=> 'submodule',
967 'indexpageids' => false,
969 'exportnowrap' => false,
971 ParamValidator
::PARAM_DEFAULT
=> WikiExporter
::schemaVersion(),
972 ParamValidator
::PARAM_TYPE
=> XmlDumpWriter
::$supportedSchemas,
976 ApiBase
::PARAM_HELP_MSG
=> 'api-help-param-continue',
978 'rawcontinue' => false,
981 $result +
= $this->getPageSet()->getFinalParams( $flags );
987 public function isReadMode() {
988 // We need to make an exception for certain meta modules that should be
989 // accessible even without the 'read' right. Restrict the exception as
990 // much as possible: no other modules allowed, and no pageset
991 // parameters either. We do allow the 'rawcontinue' and 'indexpageids'
992 // parameters since frameworks might add these unconditionally and they
993 // can't expose anything here.
994 $allowedParams = [ 'rawcontinue' => 1, 'indexpageids' => 1 ];
995 $this->mParams
= $this->extractRequestParams();
996 $request = $this->getRequest();
997 foreach ( $this->mParams +
$this->getPageSet()->extractRequestParams() as $param => $value ) {
998 $needed = $param === 'meta';
999 if ( !isset( $allowedParams[$param] ) && $request->getCheck( $param ) !== $needed ) {
1004 // Ask each module if it requires read mode. Any true => this returns
1007 $this->instantiateModules( $modules, 'meta' );
1008 foreach ( $modules as $module ) {
1009 if ( $module->isReadMode() ) {
1017 public function isWriteMode() {
1018 // Ask each module if it requires write mode. If any require write mode this returns true.
1020 $this->mParams
= $this->extractRequestParams();
1021 $this->instantiateModules( $modules, 'list' );
1022 $this->instantiateModules( $modules, 'meta' );
1023 $this->instantiateModules( $modules, 'prop' );
1024 foreach ( $modules as $module ) {
1025 if ( $module->isWriteMode() ) {
1033 protected function getExamplesMessages() {
1034 $title = Title
::newMainPage()->getPrefixedText();
1035 $mp = rawurlencode( $title );
1038 'action=query&prop=revisions&meta=siteinfo&' .
1039 "titles={$mp}&rvprop=user|comment&continue="
1040 => 'apihelp-query-example-revisions',
1041 'action=query&generator=allpages&gapprefix=API/&prop=revisions&continue='
1042 => 'apihelp-query-example-allpages',
1046 public function getHelpUrls() {
1048 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Query',
1049 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Meta',
1050 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Properties',
1051 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Lists',
1056 /** @deprecated class alias since 1.43 */
1057 class_alias( ApiQuery
::class, 'ApiQuery' );