Merge "ResourceLoader: Deprecate ResourceLoader::makeConfigSetScript"
[mediawiki.git] / includes / api / ApiQuerySiteinfo.php
blob4fe733a09ffee6c87bb3cdd7e46148e4b7ee805a
1 <?php
2 /**
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
20 * @file
23 namespace MediaWiki\Api;
25 use MediaWiki\HookContainer\HookContainer;
26 use MediaWiki\Interwiki\InterwikiLookup;
27 use MediaWiki\Language\Language;
28 use MediaWiki\Language\LanguageCode;
29 use MediaWiki\Language\LanguageConverter;
30 use MediaWiki\Languages\LanguageConverterFactory;
31 use MediaWiki\Languages\LanguageFactory;
32 use MediaWiki\Languages\LanguageNameUtils;
33 use MediaWiki\MainConfigNames;
34 use MediaWiki\Parser\MagicWordFactory;
35 use MediaWiki\Parser\ParserFactory;
36 use MediaWiki\Registration\ExtensionRegistry;
37 use MediaWiki\ResourceLoader\SkinModule;
38 use MediaWiki\SiteStats\SiteStats;
39 use MediaWiki\SpecialPage\SpecialPage;
40 use MediaWiki\SpecialPage\SpecialPageFactory;
41 use MediaWiki\Specials\SpecialVersion;
42 use MediaWiki\Title\NamespaceInfo;
43 use MediaWiki\Title\Title;
44 use MediaWiki\User\Options\UserOptionsLookup;
45 use MediaWiki\User\TempUser\TempUserConfig;
46 use MediaWiki\User\UserGroupManager;
47 use MediaWiki\Utils\ExtensionInfo;
48 use MediaWiki\Utils\GitInfo;
49 use MediaWiki\Utils\UrlUtils;
50 use MediaWiki\WikiMap\WikiMap;
51 use Skin;
52 use SkinFactory;
53 use UploadBase;
54 use Wikimedia\Composer\ComposerInstalled;
55 use Wikimedia\ParamValidator\ParamValidator;
56 use Wikimedia\Rdbms\ILoadBalancer;
57 use Wikimedia\Rdbms\ReadOnlyMode;
59 /**
60 * A query action to return meta information about the wiki site.
62 * @ingroup API
64 class ApiQuerySiteinfo extends ApiQueryBase {
66 private UserOptionsLookup $userOptionsLookup;
67 private UserGroupManager $userGroupManager;
68 private HookContainer $hookContainer;
69 private LanguageConverterFactory $languageConverterFactory;
70 private LanguageFactory $languageFactory;
71 private LanguageNameUtils $languageNameUtils;
72 private Language $contentLanguage;
73 private NamespaceInfo $namespaceInfo;
74 private InterwikiLookup $interwikiLookup;
75 private ParserFactory $parserFactory;
76 private MagicWordFactory $magicWordFactory;
77 private SpecialPageFactory $specialPageFactory;
78 private SkinFactory $skinFactory;
79 private ILoadBalancer $loadBalancer;
80 private ReadOnlyMode $readOnlyMode;
81 private UrlUtils $urlUtils;
82 private TempUserConfig $tempUserConfig;
84 public function __construct(
85 ApiQuery $query,
86 string $moduleName,
87 UserOptionsLookup $userOptionsLookup,
88 UserGroupManager $userGroupManager,
89 HookContainer $hookContainer,
90 LanguageConverterFactory $languageConverterFactory,
91 LanguageFactory $languageFactory,
92 LanguageNameUtils $languageNameUtils,
93 Language $contentLanguage,
94 NamespaceInfo $namespaceInfo,
95 InterwikiLookup $interwikiLookup,
96 ParserFactory $parserFactory,
97 MagicWordFactory $magicWordFactory,
98 SpecialPageFactory $specialPageFactory,
99 SkinFactory $skinFactory,
100 ILoadBalancer $loadBalancer,
101 ReadOnlyMode $readOnlyMode,
102 UrlUtils $urlUtils,
103 TempUserConfig $tempUserConfig
105 parent::__construct( $query, $moduleName, 'si' );
106 $this->userOptionsLookup = $userOptionsLookup;
107 $this->userGroupManager = $userGroupManager;
108 $this->hookContainer = $hookContainer;
109 $this->languageConverterFactory = $languageConverterFactory;
110 $this->languageFactory = $languageFactory;
111 $this->languageNameUtils = $languageNameUtils;
112 $this->contentLanguage = $contentLanguage;
113 $this->namespaceInfo = $namespaceInfo;
114 $this->interwikiLookup = $interwikiLookup;
115 $this->parserFactory = $parserFactory;
116 $this->magicWordFactory = $magicWordFactory;
117 $this->specialPageFactory = $specialPageFactory;
118 $this->skinFactory = $skinFactory;
119 $this->loadBalancer = $loadBalancer;
120 $this->readOnlyMode = $readOnlyMode;
121 $this->urlUtils = $urlUtils;
122 $this->tempUserConfig = $tempUserConfig;
125 public function execute() {
126 $params = $this->extractRequestParams();
127 $done = [];
128 foreach ( $params['prop'] as $p ) {
129 switch ( $p ) {
130 case 'general':
131 $fit = $this->appendGeneralInfo( $p );
132 break;
133 case 'namespaces':
134 $fit = $this->appendNamespaces( $p );
135 break;
136 case 'namespacealiases':
137 $fit = $this->appendNamespaceAliases( $p );
138 break;
139 case 'specialpagealiases':
140 $fit = $this->appendSpecialPageAliases( $p );
141 break;
142 case 'magicwords':
143 $fit = $this->appendMagicWords( $p );
144 break;
145 case 'interwikimap':
146 $fit = $this->appendInterwikiMap( $p, $params['filteriw'] );
147 break;
148 case 'dbrepllag':
149 $fit = $this->appendDbReplLagInfo( $p, $params['showalldb'] );
150 break;
151 case 'statistics':
152 $fit = $this->appendStatistics( $p );
153 break;
154 case 'usergroups':
155 $fit = $this->appendUserGroups( $p, $params['numberingroup'] );
156 break;
157 case 'autocreatetempuser':
158 $fit = $this->appendAutoCreateTempUser( $p );
159 break;
160 case 'clientlibraries':
161 $fit = $this->appendInstalledClientLibraries( $p );
162 break;
163 case 'libraries':
164 $fit = $this->appendInstalledLibraries( $p );
165 break;
166 case 'extensions':
167 $fit = $this->appendExtensions( $p );
168 break;
169 case 'fileextensions':
170 $fit = $this->appendFileExtensions( $p );
171 break;
172 case 'rightsinfo':
173 $fit = $this->appendRightsInfo( $p );
174 break;
175 case 'restrictions':
176 $fit = $this->appendRestrictions( $p );
177 break;
178 case 'languages':
179 $fit = $this->appendLanguages( $p );
180 break;
181 case 'languagevariants':
182 $fit = $this->appendLanguageVariants( $p );
183 break;
184 case 'skins':
185 $fit = $this->appendSkins( $p );
186 break;
187 case 'extensiontags':
188 $fit = $this->appendExtensionTags( $p );
189 break;
190 case 'functionhooks':
191 $fit = $this->appendFunctionHooks( $p );
192 break;
193 case 'showhooks':
194 $fit = $this->appendSubscribedHooks( $p );
195 break;
196 case 'variables':
197 $fit = $this->appendVariables( $p );
198 break;
199 case 'protocols':
200 $fit = $this->appendProtocols( $p );
201 break;
202 case 'defaultoptions':
203 $fit = $this->appendDefaultOptions( $p );
204 break;
205 case 'uploaddialog':
206 $fit = $this->appendUploadDialog( $p );
207 break;
208 case 'autopromote':
209 $fit = $this->appendAutoPromote( $p );
210 break;
211 case 'autopromoteonce':
212 $fit = $this->appendAutoPromoteOnce( $p );
213 break;
214 default:
215 ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" ); // @codeCoverageIgnore
217 if ( !$fit ) {
218 // Abuse siprop as a query-continue parameter
219 // and set it to all unprocessed props
220 $this->setContinueEnumParameter( 'prop', implode( '|',
221 array_diff( $params['prop'], $done ) ) );
222 break;
224 $done[] = $p;
228 protected function appendGeneralInfo( $property ) {
229 $config = $this->getConfig();
230 $mainPage = Title::newMainPage();
231 $logo = SkinModule::getAvailableLogos( $config, $this->getLanguage()->getCode() );
233 $data = [
234 'mainpage' => $mainPage->getPrefixedText(),
235 'base' => (string)$this->urlUtils->expand( $mainPage->getFullURL(), PROTO_CURRENT ),
236 'sitename' => $config->get( MainConfigNames::Sitename ),
237 'mainpageisdomainroot' => (bool)$config->get( MainConfigNames::MainPageIsDomainRoot ),
239 // A logo can either be a relative or an absolute path
240 // make sure we always return an absolute path
241 'logo' => (string)$this->urlUtils->expand( $logo['1x'], PROTO_RELATIVE ),
243 'generator' => 'MediaWiki ' . MW_VERSION,
245 'phpversion' => PHP_VERSION,
246 'phpsapi' => PHP_SAPI,
247 'dbtype' => $config->get( MainConfigNames::DBtype ),
248 'dbversion' => $this->getDB()->getServerVersion(),
251 $allowFrom = [ '' ];
252 $allowException = true;
253 if ( !$config->get( MainConfigNames::AllowExternalImages ) ) {
254 $data['imagewhitelistenabled'] =
255 (bool)$config->get( MainConfigNames::EnableImageWhitelist );
256 $allowFrom = $config->get( MainConfigNames::AllowExternalImagesFrom );
257 $allowException = (bool)$allowFrom;
259 if ( $allowException ) {
260 $data['externalimages'] = (array)$allowFrom;
261 ApiResult::setIndexedTagName( $data['externalimages'], 'prefix' );
264 $data['langconversion'] = !$this->languageConverterFactory->isConversionDisabled();
265 $data['linkconversion'] = !$this->languageConverterFactory->isLinkConversionDisabled();
266 // For backwards compatibility (soft deprecated since MW 1.36)
267 $data['titleconversion'] = $data['linkconversion'];
269 $contLangConverter = $this->languageConverterFactory->getLanguageConverter( $this->contentLanguage );
270 if ( $this->contentLanguage->linkPrefixExtension() ) {
271 $linkPrefixCharset = $this->contentLanguage->linkPrefixCharset();
272 $data['linkprefixcharset'] = $linkPrefixCharset;
273 // For backwards compatibility
274 $data['linkprefix'] = "/^((?>.*[^$linkPrefixCharset]|))(.+)$/sDu";
275 } else {
276 $data['linkprefixcharset'] = '';
277 $data['linkprefix'] = '';
280 $data['linktrail'] = $this->contentLanguage->linkTrail() ?: '';
282 $data['legaltitlechars'] = Title::legalChars();
283 $data['invalidusernamechars'] = $config->get( MainConfigNames::InvalidUsernameCharacters );
285 $data['allunicodefixes'] = (bool)$config->get( MainConfigNames::AllUnicodeFixes );
286 $data['fixarabicunicode'] = true; // Config removed in 1.35, always true
287 $data['fixmalayalamunicode'] = true; // Config removed in 1.35, always true
289 $git = GitInfo::repo()->getHeadSHA1();
290 if ( $git ) {
291 $data['git-hash'] = $git;
292 $data['git-branch'] = GitInfo::repo()->getCurrentBranch();
295 // 'case-insensitive' option is reserved for future
296 $data['case'] =
297 $config->get( MainConfigNames::CapitalLinks ) ? 'first-letter' : 'case-sensitive';
298 $data['lang'] = $config->get( MainConfigNames::LanguageCode );
300 $data['fallback'] = [];
301 foreach ( $this->contentLanguage->getFallbackLanguages() as $code ) {
302 $data['fallback'][] = [ 'code' => $code ];
304 ApiResult::setIndexedTagName( $data['fallback'], 'lang' );
306 if ( $contLangConverter->hasVariants() ) {
307 $data['variants'] = [];
308 foreach ( $contLangConverter->getVariants() as $code ) {
309 $data['variants'][] = [
310 'code' => $code,
311 'name' => $this->contentLanguage->getVariantname( $code ),
314 ApiResult::setIndexedTagName( $data['variants'], 'lang' );
317 $data['rtl'] = $this->contentLanguage->isRTL();
318 $data['fallback8bitEncoding'] = $this->contentLanguage->fallback8bitEncoding();
320 $data['readonly'] = $this->readOnlyMode->isReadOnly();
321 if ( $data['readonly'] ) {
322 $data['readonlyreason'] = $this->readOnlyMode->getReason();
324 $data['writeapi'] = true; // Deprecated since MW 1.32
326 $data['maxarticlesize'] = $config->get( MainConfigNames::MaxArticleSize ) * 1024;
328 $data['timezone'] = $config->get( MainConfigNames::Localtimezone );
329 $data['timeoffset'] = (int)( $config->get( MainConfigNames::LocalTZoffset ) );
330 $data['articlepath'] = $config->get( MainConfigNames::ArticlePath );
331 $data['scriptpath'] = $config->get( MainConfigNames::ScriptPath );
332 $data['script'] = $config->get( MainConfigNames::Script );
333 $data['variantarticlepath'] = $config->get( MainConfigNames::VariantArticlePath );
334 $data[ApiResult::META_BC_BOOLS][] = 'variantarticlepath';
335 $data['server'] = $config->get( MainConfigNames::Server );
336 $data['servername'] = $config->get( MainConfigNames::ServerName );
337 $data['wikiid'] = WikiMap::getCurrentWikiId();
338 $data['time'] = wfTimestamp( TS_ISO_8601, time() );
340 $data['misermode'] = (bool)$config->get( MainConfigNames::MiserMode );
342 $data['uploadsenabled'] = UploadBase::isEnabled();
343 $data['maxuploadsize'] = UploadBase::getMaxUploadSize();
344 $data['minuploadchunksize'] = ApiUpload::getMinUploadChunkSize( $config );
346 $data['galleryoptions'] = $config->get( MainConfigNames::GalleryOptions );
348 $data['thumblimits'] = $config->get( MainConfigNames::ThumbLimits );
349 ApiResult::setArrayType( $data['thumblimits'], 'BCassoc' );
350 ApiResult::setIndexedTagName( $data['thumblimits'], 'limit' );
351 $data['imagelimits'] = [];
352 foreach ( $config->get( MainConfigNames::ImageLimits ) as $k => $limit ) {
353 $data['imagelimits'][$k] = [ 'width' => $limit[0], 'height' => $limit[1] ];
355 ApiResult::setArrayType( $data['imagelimits'], 'BCassoc' );
356 ApiResult::setIndexedTagName( $data['imagelimits'], 'limit' );
358 $favicon = $config->get( MainConfigNames::Favicon );
359 if ( $favicon ) {
360 // Expand any local path to full URL to improve API usability (T77093).
361 $data['favicon'] = (string)$this->urlUtils->expand( $favicon );
364 $data['centralidlookupprovider'] = $config->get( MainConfigNames::CentralIdLookupProvider );
365 $providerIds = array_keys( $config->get( MainConfigNames::CentralIdLookupProviders ) );
366 $data['allcentralidlookupproviders'] = $providerIds;
368 $data['interwikimagic'] = (bool)$config->get( MainConfigNames::InterwikiMagic );
369 $data['magiclinks'] = $config->get( MainConfigNames::EnableMagicLinks );
371 $data['categorycollation'] = $config->get( MainConfigNames::CategoryCollation );
373 $data['nofollowlinks'] = $config->get( MainConfigNames::NoFollowLinks );
374 $data['nofollownsexceptions'] = $config->get( MainConfigNames::NoFollowNsExceptions );
375 $data['nofollowdomainexceptions'] = $config->get( MainConfigNames::NoFollowDomainExceptions );
376 $data['externallinktarget'] = $config->get( MainConfigNames::ExternalLinkTarget );
378 $this->getHookRunner()->onAPIQuerySiteInfoGeneralInfo( $this, $data );
380 return $this->getResult()->addValue( 'query', $property, $data );
383 protected function appendNamespaces( $property ) {
384 $nsProtection = $this->getConfig()->get( MainConfigNames::NamespaceProtection );
386 $data = [ ApiResult::META_TYPE => 'assoc' ];
387 foreach ( $this->contentLanguage->getFormattedNamespaces() as $ns => $title ) {
388 $data[$ns] = [
389 'id' => (int)$ns,
390 'case' => $this->namespaceInfo->isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
392 ApiResult::setContentValue( $data[$ns], 'name', $title );
393 $canonical = $this->namespaceInfo->getCanonicalName( $ns );
395 $data[$ns]['subpages'] = $this->namespaceInfo->hasSubpages( $ns );
397 if ( $canonical ) {
398 $data[$ns]['canonical'] = strtr( $canonical, '_', ' ' );
401 $data[$ns]['content'] = $this->namespaceInfo->isContent( $ns );
402 $data[$ns]['nonincludable'] = $this->namespaceInfo->isNonincludable( $ns );
404 $specificNs = $nsProtection[$ns] ?? '';
405 if ( is_array( $specificNs ) ) {
406 $specificNs = implode( "|", array_filter( $specificNs ) );
408 if ( $specificNs !== '' ) {
409 $data[$ns]['namespaceprotection'] = $specificNs;
412 $contentmodel = $this->namespaceInfo->getNamespaceContentModel( $ns );
413 if ( $contentmodel ) {
414 $data[$ns]['defaultcontentmodel'] = $contentmodel;
418 ApiResult::setArrayType( $data, 'assoc' );
419 ApiResult::setIndexedTagName( $data, 'ns' );
421 return $this->getResult()->addValue( 'query', $property, $data );
424 protected function appendNamespaceAliases( $property ) {
425 $aliases = $this->contentLanguage->getNamespaceAliases();
426 $namespaces = $this->contentLanguage->getNamespaces();
427 $data = [];
428 foreach ( $aliases as $title => $ns ) {
429 if ( $namespaces[$ns] == $title ) {
430 // Don't list duplicates
431 continue;
433 $item = [ 'id' => (int)$ns ];
434 ApiResult::setContentValue( $item, 'alias', strtr( $title, '_', ' ' ) );
435 $data[] = $item;
438 sort( $data );
440 ApiResult::setIndexedTagName( $data, 'ns' );
442 return $this->getResult()->addValue( 'query', $property, $data );
445 protected function appendSpecialPageAliases( $property ) {
446 $data = [];
447 $aliases = $this->contentLanguage->getSpecialPageAliases();
448 foreach ( $this->specialPageFactory->getNames() as $specialpage ) {
449 if ( isset( $aliases[$specialpage] ) ) {
450 $arr = [ 'realname' => $specialpage, 'aliases' => $aliases[$specialpage] ];
451 ApiResult::setIndexedTagName( $arr['aliases'], 'alias' );
452 $data[] = $arr;
455 ApiResult::setIndexedTagName( $data, 'specialpage' );
457 return $this->getResult()->addValue( 'query', $property, $data );
460 protected function appendMagicWords( $property ) {
461 $data = [];
462 foreach ( $this->contentLanguage->getMagicWords() as $name => $aliases ) {
463 $caseSensitive = (bool)array_shift( $aliases );
464 $arr = [
465 'name' => $name,
466 'aliases' => $aliases,
467 'case-sensitive' => $caseSensitive,
469 ApiResult::setIndexedTagName( $arr['aliases'], 'alias' );
470 $data[] = $arr;
472 ApiResult::setIndexedTagName( $data, 'magicword' );
474 return $this->getResult()->addValue( 'query', $property, $data );
477 protected function appendInterwikiMap( $property, $filter ) {
478 $local = $filter ? $filter === 'local' : null;
480 $params = $this->extractRequestParams();
481 $langCode = $params['inlanguagecode'] ?? '';
482 $interwikiMagic = $this->getConfig()->get( MainConfigNames::InterwikiMagic );
484 if ( $interwikiMagic ) {
485 $langNames = $this->languageNameUtils->getLanguageNames( $langCode );
488 $extraLangPrefixes = $this->getConfig()->get( MainConfigNames::ExtraInterlanguageLinkPrefixes );
489 $extraLangCodeMap = $this->getConfig()->get( MainConfigNames::InterlanguageLinkCodeMap );
490 $localInterwikis = $this->getConfig()->get( MainConfigNames::LocalInterwikis );
491 $data = [];
493 foreach ( $this->interwikiLookup->getAllPrefixes( $local ) as $row ) {
494 $prefix = $row['iw_prefix'];
495 $val = [];
496 $val['prefix'] = $prefix;
497 if ( $row['iw_local'] ?? false ) {
498 $val['local'] = true;
500 if ( $row['iw_trans'] ?? false ) {
501 $val['trans'] = true;
504 if ( $interwikiMagic && isset( $langNames[$prefix] ) ) {
505 $val['language'] = $langNames[$prefix];
506 $standard = LanguageCode::replaceDeprecatedCodes( $prefix );
507 if ( $standard !== $prefix ) {
508 # Note that even if this code is deprecated, it should
509 # only be remapped if extralanglink (set below) is false.
510 $val['deprecated'] = $standard;
512 $val['bcp47'] = LanguageCode::bcp47( $standard );
514 if ( in_array( $prefix, $localInterwikis ) ) {
515 $val['localinterwiki'] = true;
517 if ( $interwikiMagic && in_array( $prefix, $extraLangPrefixes ) ) {
518 $val['extralanglink'] = true;
519 $val['code'] = $extraLangCodeMap[$prefix] ?? $prefix;
520 $val['bcp47'] = LanguageCode::bcp47( $val['code'] );
522 $linktext = $this->msg( "interlanguage-link-$prefix" );
523 if ( !$linktext->isDisabled() ) {
524 $val['linktext'] = $linktext->text();
527 $sitename = $this->msg( "interlanguage-link-sitename-$prefix" );
528 if ( !$sitename->isDisabled() ) {
529 $val['sitename'] = $sitename->text();
533 $val['url'] = (string)$this->urlUtils->expand( $row['iw_url'], PROTO_CURRENT );
534 $val['protorel'] = str_starts_with( $row['iw_url'], '//' );
535 if ( ( $row['iw_wikiid'] ?? '' ) !== '' ) {
536 $val['wikiid'] = $row['iw_wikiid'];
538 if ( ( $row['iw_api'] ?? '' ) !== '' ) {
539 $val['api'] = $row['iw_api'];
542 $data[] = $val;
545 ApiResult::setIndexedTagName( $data, 'iw' );
547 return $this->getResult()->addValue( 'query', $property, $data );
550 protected function appendDbReplLagInfo( $property, $includeAll ) {
551 $data = [];
552 $showHostnames = $this->getConfig()->get( MainConfigNames::ShowHostnames );
553 if ( $includeAll ) {
554 if ( !$showHostnames ) {
555 $this->dieWithError( 'apierror-siteinfo-includealldenied', 'includeAllDenied' );
558 foreach ( $this->loadBalancer->getLagTimes() as $i => $lag ) {
559 $data[] = [
560 'host' => $this->loadBalancer->getServerName( $i ),
561 'lag' => $lag
564 } else {
565 [ , $lag, $index ] = $this->loadBalancer->getMaxLag();
566 $data[] = [
567 'host' => $showHostnames ? $this->loadBalancer->getServerName( $index ) : '',
568 'lag' => $lag
572 ApiResult::setIndexedTagName( $data, 'db' );
574 return $this->getResult()->addValue( 'query', $property, $data );
577 protected function appendStatistics( $property ) {
578 $data = [
579 'pages' => SiteStats::pages(),
580 'articles' => SiteStats::articles(),
581 'edits' => SiteStats::edits(),
582 'images' => SiteStats::images(),
583 'users' => SiteStats::users(),
584 'activeusers' => SiteStats::activeUsers(),
585 'admins' => SiteStats::numberingroup( 'sysop' ),
586 'jobs' => SiteStats::jobs(),
589 $this->getHookRunner()->onAPIQuerySiteInfoStatisticsInfo( $data );
591 return $this->getResult()->addValue( 'query', $property, $data );
594 protected function appendUserGroups( $property, $numberInGroup ) {
595 $config = $this->getConfig();
597 $data = [];
598 $result = $this->getResult();
599 $allGroups = array_values( $this->userGroupManager->listAllGroups() );
600 foreach ( $config->get( MainConfigNames::GroupPermissions ) as $group => $permissions ) {
601 $arr = [
602 'name' => $group,
603 'rights' => array_keys( $permissions, true ),
606 if ( $numberInGroup ) {
607 $autopromote = $config->get( MainConfigNames::Autopromote );
609 if ( $group == 'user' ) {
610 $arr['number'] = SiteStats::users();
611 // '*' and autopromote groups have no size
612 } elseif ( $group !== '*' && !isset( $autopromote[$group] ) ) {
613 $arr['number'] = SiteStats::numberingroup( $group );
617 $groupArr = [
618 'add' => $config->get( MainConfigNames::AddGroups ),
619 'remove' => $config->get( MainConfigNames::RemoveGroups ),
620 'add-self' => $config->get( MainConfigNames::GroupsAddToSelf ),
621 'remove-self' => $config->get( MainConfigNames::GroupsRemoveFromSelf )
624 foreach ( $groupArr as $type => $rights ) {
625 if ( isset( $rights[$group] ) ) {
626 if ( $rights[$group] === true ) {
627 $groups = $allGroups;
628 } else {
629 $groups = array_intersect( $rights[$group], $allGroups );
631 if ( $groups ) {
632 $arr[$type] = $groups;
633 ApiResult::setArrayType( $arr[$type], 'BCarray' );
634 ApiResult::setIndexedTagName( $arr[$type], 'group' );
639 ApiResult::setIndexedTagName( $arr['rights'], 'permission' );
640 $data[] = $arr;
643 ApiResult::setIndexedTagName( $data, 'group' );
645 return $result->addValue( 'query', $property, $data );
648 protected function appendAutoCreateTempUser( $property ) {
649 $data = [ 'enabled' => $this->tempUserConfig->isEnabled() ];
650 if ( $this->tempUserConfig->isKnown() ) {
651 $data['matchPatterns'] = $this->tempUserConfig->getMatchPatterns();
653 return $this->getResult()->addValue( 'query', $property, $data );
656 protected function appendFileExtensions( $property ) {
657 $data = [];
658 foreach (
659 array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) ) as $ext
661 $data[] = [ 'ext' => $ext ];
663 ApiResult::setIndexedTagName( $data, 'fe' );
665 return $this->getResult()->addValue( 'query', $property, $data );
668 protected function appendInstalledClientLibraries( $property ) {
669 $data = [];
670 foreach ( SpecialVersion::parseForeignResources() as $name => $info ) {
671 $data[] = [
672 // Can't use $name as it is version suffixed (as multiple versions
673 // of a library may exist, provided by different skins/extensions)
674 'name' => $info['name'],
675 'version' => $info['version'],
678 ApiResult::setIndexedTagName( $data, 'library' );
679 return $this->getResult()->addValue( 'query', $property, $data );
682 protected function appendInstalledLibraries( $property ) {
683 $path = MW_INSTALL_PATH . '/vendor/composer/installed.json';
684 if ( !file_exists( $path ) ) {
685 return true;
688 $data = [];
689 $installed = new ComposerInstalled( $path );
690 foreach ( $installed->getInstalledDependencies() as $name => $info ) {
691 if ( str_starts_with( $info['type'], 'mediawiki-' ) ) {
692 // Skip any extensions or skins since they'll be listed
693 // in their proper section
694 continue;
696 $data[] = [
697 'name' => $name,
698 'version' => $info['version'],
701 ApiResult::setIndexedTagName( $data, 'library' );
703 return $this->getResult()->addValue( 'query', $property, $data );
706 protected function appendExtensions( $property ) {
707 $data = [];
708 $credits = SpecialVersion::getCredits(
709 ExtensionRegistry::getInstance(),
710 $this->getConfig()
712 foreach ( $credits as $type => $extensions ) {
713 foreach ( $extensions as $ext ) {
714 $ret = [ 'type' => $type ];
715 if ( isset( $ext['name'] ) ) {
716 $ret['name'] = $ext['name'];
718 if ( isset( $ext['namemsg'] ) ) {
719 $ret['namemsg'] = $ext['namemsg'];
721 if ( isset( $ext['description'] ) ) {
722 $ret['description'] = $ext['description'];
724 if ( isset( $ext['descriptionmsg'] ) ) {
725 // Can be a string or [ key, param1, param2, ... ]
726 if ( is_array( $ext['descriptionmsg'] ) ) {
727 $ret['descriptionmsg'] = $ext['descriptionmsg'][0];
728 $ret['descriptionmsgparams'] = array_slice( $ext['descriptionmsg'], 1 );
729 ApiResult::setIndexedTagName( $ret['descriptionmsgparams'], 'param' );
730 } else {
731 $ret['descriptionmsg'] = $ext['descriptionmsg'];
734 if ( isset( $ext['author'] ) ) {
735 $ret['author'] = is_array( $ext['author'] ) ?
736 implode( ', ', $ext['author'] ) : $ext['author'];
738 if ( isset( $ext['url'] ) ) {
739 $ret['url'] = $ext['url'];
741 if ( isset( $ext['version'] ) ) {
742 $ret['version'] = $ext['version'];
744 if ( isset( $ext['path'] ) ) {
745 $extensionPath = dirname( $ext['path'] );
746 $gitInfo = new GitInfo( $extensionPath );
747 $vcsVersion = $gitInfo->getHeadSHA1();
748 if ( $vcsVersion !== false ) {
749 $ret['vcs-system'] = 'git';
750 $ret['vcs-version'] = $vcsVersion;
751 $ret['vcs-url'] = $gitInfo->getHeadViewUrl();
752 $vcsDate = $gitInfo->getHeadCommitDate();
753 if ( $vcsDate !== false ) {
754 $ret['vcs-date'] = wfTimestamp( TS_ISO_8601, $vcsDate );
758 if ( ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
759 $ret['license-name'] = $ext['license-name'] ?? '';
760 $ret['license'] = SpecialPage::getTitleFor(
761 'Version',
762 "License/{$ext['name']}"
763 )->getLinkURL();
766 if ( ExtensionInfo::getAuthorsFileName( $extensionPath ) ) {
767 $ret['credits'] = SpecialPage::getTitleFor(
768 'Version',
769 "Credits/{$ext['name']}"
770 )->getLinkURL();
773 $data[] = $ret;
777 ApiResult::setIndexedTagName( $data, 'ext' );
779 return $this->getResult()->addValue( 'query', $property, $data );
782 protected function appendRightsInfo( $property ) {
783 $config = $this->getConfig();
784 $title = Title::newFromText( $config->get( MainConfigNames::RightsPage ) );
785 if ( $title ) {
786 $url = $this->urlUtils->expand( $title->getLinkURL(), PROTO_CURRENT );
787 } else {
788 $url = $config->get( MainConfigNames::RightsUrl );
790 $text = $config->get( MainConfigNames::RightsText ) ?? '';
791 if ( $text === '' && $title ) {
792 $text = $title->getPrefixedText();
795 $data = [
796 'url' => (string)$url,
797 'text' => (string)$text,
800 return $this->getResult()->addValue( 'query', $property, $data );
803 protected function appendRestrictions( $property ) {
804 $config = $this->getConfig();
805 $data = [
806 'types' => $config->get( MainConfigNames::RestrictionTypes ),
807 'levels' => $config->get( MainConfigNames::RestrictionLevels ),
808 'cascadinglevels' => $config->get( MainConfigNames::CascadingRestrictionLevels ),
809 'semiprotectedlevels' => $config->get( MainConfigNames::SemiprotectedRestrictionLevels ),
812 ApiResult::setArrayType( $data['types'], 'BCarray' );
813 ApiResult::setArrayType( $data['levels'], 'BCarray' );
814 ApiResult::setArrayType( $data['cascadinglevels'], 'BCarray' );
815 ApiResult::setArrayType( $data['semiprotectedlevels'], 'BCarray' );
817 ApiResult::setIndexedTagName( $data['types'], 'type' );
818 ApiResult::setIndexedTagName( $data['levels'], 'level' );
819 ApiResult::setIndexedTagName( $data['cascadinglevels'], 'level' );
820 ApiResult::setIndexedTagName( $data['semiprotectedlevels'], 'level' );
822 return $this->getResult()->addValue( 'query', $property, $data );
825 public function appendLanguages( $property ) {
826 $params = $this->extractRequestParams();
827 $langCode = $params['inlanguagecode'] ?? '';
828 $langNames = $this->languageNameUtils->getLanguageNames( $langCode );
830 $data = [];
832 foreach ( $langNames as $code => $name ) {
833 $lang = [
834 'code' => $code,
835 'bcp47' => LanguageCode::bcp47( $code ),
837 ApiResult::setContentValue( $lang, 'name', $name );
838 $data[] = $lang;
840 ApiResult::setIndexedTagName( $data, 'lang' );
842 return $this->getResult()->addValue( 'query', $property, $data );
845 // Export information about which page languages will trigger
846 // language conversion. (T153341)
847 public function appendLanguageVariants( $property ) {
848 $langNames = $this->languageConverterFactory->isConversionDisabled() ? [] :
849 LanguageConverter::$languagesWithVariants;
850 sort( $langNames );
852 $data = [];
853 foreach ( $langNames as $langCode ) {
854 $lang = $this->languageFactory->getLanguage( $langCode );
855 $langConverter = $this->languageConverterFactory->getLanguageConverter( $lang );
856 if ( !$langConverter->hasVariants() ) {
857 // Only languages which have variants should be listed
858 continue;
860 $data[$langCode] = [];
861 ApiResult::setIndexedTagName( $data[$langCode], 'variant' );
862 ApiResult::setArrayType( $data[$langCode], 'kvp', 'code' );
864 $variants = $langConverter->getVariants();
865 sort( $variants );
866 foreach ( $variants as $v ) {
867 $data[$langCode][$v] = [
868 'fallbacks' => (array)$langConverter->getVariantFallbacks( $v ),
870 ApiResult::setIndexedTagName(
871 $data[$langCode][$v]['fallbacks'], 'variant'
875 ApiResult::setIndexedTagName( $data, 'lang' );
876 ApiResult::setArrayType( $data, 'kvp', 'code' );
878 return $this->getResult()->addValue( 'query', $property, $data );
881 public function appendSkins( $property ) {
882 $data = [];
883 $allowed = $this->skinFactory->getAllowedSkins();
884 $default = Skin::normalizeKey( 'default' );
886 foreach ( $this->skinFactory->getInstalledSkins() as $name => $displayName ) {
887 $msg = $this->msg( "skinname-{$name}" );
888 $code = $this->getParameter( 'inlanguagecode' );
889 if ( $code && $this->languageNameUtils->isValidCode( $code ) ) {
890 $msg->inLanguage( $code );
891 } else {
892 $msg->inContentLanguage();
894 if ( $msg->exists() ) {
895 $displayName = $msg->text();
897 $skin = [ 'code' => $name ];
898 ApiResult::setContentValue( $skin, 'name', $displayName );
899 if ( !isset( $allowed[$name] ) ) {
900 $skin['unusable'] = true;
902 if ( $name === $default ) {
903 $skin['default'] = true;
905 $data[] = $skin;
907 ApiResult::setIndexedTagName( $data, 'skin' );
909 return $this->getResult()->addValue( 'query', $property, $data );
912 public function appendExtensionTags( $property ) {
913 $tags = array_map(
914 static function ( $item ) {
915 return "<$item>";
917 $this->parserFactory->getMainInstance()->getTags()
919 ApiResult::setArrayType( $tags, 'BCarray' );
920 ApiResult::setIndexedTagName( $tags, 't' );
922 return $this->getResult()->addValue( 'query', $property, $tags );
925 public function appendFunctionHooks( $property ) {
926 $hooks = $this->parserFactory->getMainInstance()->getFunctionHooks();
927 ApiResult::setArrayType( $hooks, 'BCarray' );
928 ApiResult::setIndexedTagName( $hooks, 'h' );
930 return $this->getResult()->addValue( 'query', $property, $hooks );
933 public function appendVariables( $property ) {
934 $variables = $this->magicWordFactory->getVariableIDs();
935 ApiResult::setArrayType( $variables, 'BCarray' );
936 ApiResult::setIndexedTagName( $variables, 'v' );
938 return $this->getResult()->addValue( 'query', $property, $variables );
941 public function appendProtocols( $property ) {
942 // Make a copy of the global so we don't try to set the _element key of it - T47130
943 $protocols = array_values( $this->getConfig()->get( MainConfigNames::UrlProtocols ) );
944 ApiResult::setArrayType( $protocols, 'BCarray' );
945 ApiResult::setIndexedTagName( $protocols, 'p' );
947 return $this->getResult()->addValue( 'query', $property, $protocols );
950 public function appendDefaultOptions( $property ) {
951 $options = $this->userOptionsLookup->getDefaultOptions( null );
952 $options[ApiResult::META_BC_BOOLS] = array_keys( $options );
953 return $this->getResult()->addValue( 'query', $property, $options );
956 public function appendUploadDialog( $property ) {
957 $config = $this->getConfig()->get( MainConfigNames::UploadDialog );
958 return $this->getResult()->addValue( 'query', $property, $config );
961 private function getAutoPromoteConds() {
962 $allowedConditions = [];
963 foreach ( get_defined_constants() as $constantName => $constantValue ) {
964 if ( strpos( $constantName, 'APCOND_' ) !== false ) {
965 $allowedConditions[$constantName] = $constantValue;
968 return $allowedConditions;
971 private function processAutoPromote( $input, $allowedConditions ) {
972 $data = [];
973 foreach ( $input as $groupName => $conditions ) {
974 $row = $this->recAutopromote( $conditions, $allowedConditions );
975 if ( !isset( $row[0] ) || is_string( $row ) ) {
976 $row = [ $row ];
978 $data[$groupName] = $row;
980 return $data;
983 private function appendAutoPromote( $property ) {
984 return $this->getResult()->addValue(
985 'query',
986 $property,
987 $this->processAutoPromote(
988 $this->getConfig()->get( MainConfigNames::Autopromote ),
989 $this->getAutoPromoteConds()
994 private function appendAutoPromoteOnce( $property ) {
995 $allowedConditions = $this->getAutoPromoteConds();
996 $data = [];
997 foreach ( $this->getConfig()->get( MainConfigNames::AutopromoteOnce ) as $key => $value ) {
998 $data[$key] = $this->processAutoPromote( $value, $allowedConditions );
1000 return $this->getResult()->addValue( 'query', $property, $data );
1004 * @param array|int|string $cond
1005 * @param array $allowedConditions
1006 * @return array|string
1008 private function recAutopromote( $cond, $allowedConditions ) {
1009 $config = [];
1010 // First, checks if $cond is an array
1011 if ( is_array( $cond ) ) {
1012 // Checks if $cond[0] is a valid operand
1013 if ( in_array( $cond[0], UserGroupManager::VALID_OPS, true ) ) {
1014 $config['operand'] = $cond[0];
1015 // Traversal checks conditions
1016 foreach ( array_slice( $cond, 1 ) as $value ) {
1017 $config[] = $this->recAutopromote( $value, $allowedConditions );
1019 } elseif ( is_string( $cond[0] ) ) {
1020 // Returns $cond directly, if $cond[0] is a string
1021 $config = $cond;
1022 } else {
1023 // When $cond is equal to an APCOND_ constant value
1024 $params = array_slice( $cond, 1 );
1025 if ( $params === [ null ] ) {
1026 // Special casing for these conditions and their default of null,
1027 // to replace their values with $wgAutoConfirmCount/$wgAutoConfirmAge as appropriate
1028 if ( $cond[0] === APCOND_EDITCOUNT ) {
1029 $params = [ $this->getConfig()->get( MainConfigNames::AutoConfirmCount ) ];
1030 } elseif ( $cond[0] === APCOND_AGE ) {
1031 $params = [ $this->getConfig()->get( MainConfigNames::AutoConfirmAge ) ];
1034 $config = [
1035 'condname' => array_search( $cond[0], $allowedConditions ),
1036 'params' => $params
1038 ApiResult::setIndexedTagName( $config, 'params' );
1040 } elseif ( is_string( $cond ) ) {
1041 $config = $cond;
1042 } else {
1043 // When $cond is equal to an APCOND_ constant value
1044 $config = [
1045 'condname' => array_search( $cond, $allowedConditions ),
1046 'params' => []
1048 ApiResult::setIndexedTagName( $config, 'params' );
1051 return $config;
1054 public function appendSubscribedHooks( $property ) {
1055 $hookNames = $this->hookContainer->getHookNames();
1056 sort( $hookNames );
1058 $data = [];
1059 foreach ( $hookNames as $name ) {
1060 $arr = [
1061 'name' => $name,
1062 'subscribers' => $this->hookContainer->getHandlerDescriptions( $name ),
1065 ApiResult::setArrayType( $arr['subscribers'], 'array' );
1066 ApiResult::setIndexedTagName( $arr['subscribers'], 's' );
1067 $data[] = $arr;
1070 ApiResult::setIndexedTagName( $data, 'hook' );
1072 return $this->getResult()->addValue( 'query', $property, $data );
1075 public function getCacheMode( $params ) {
1076 // Messages for $wgExtraInterlanguageLinkPrefixes depend on user language
1077 if ( $this->getConfig()->get( MainConfigNames::ExtraInterlanguageLinkPrefixes ) &&
1078 in_array( 'interwikimap', $params['prop'] ?? [] )
1080 return 'anon-public-user-private';
1083 return 'public';
1086 public function getAllowedParams() {
1087 return [
1088 'prop' => [
1089 ParamValidator::PARAM_DEFAULT => 'general',
1090 ParamValidator::PARAM_ISMULTI => true,
1091 ParamValidator::PARAM_TYPE => [
1092 'general',
1093 'namespaces',
1094 'namespacealiases',
1095 'specialpagealiases',
1096 'magicwords',
1097 'interwikimap',
1098 'dbrepllag',
1099 'statistics',
1100 'usergroups',
1101 'autocreatetempuser',
1102 'clientlibraries',
1103 'libraries',
1104 'extensions',
1105 'fileextensions',
1106 'rightsinfo',
1107 'restrictions',
1108 'languages',
1109 'languagevariants',
1110 'skins',
1111 'extensiontags',
1112 'functionhooks',
1113 'showhooks',
1114 'variables',
1115 'protocols',
1116 'defaultoptions',
1117 'uploaddialog',
1118 'autopromote',
1119 'autopromoteonce',
1121 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
1123 'filteriw' => [
1124 ParamValidator::PARAM_TYPE => [
1125 'local',
1126 '!local',
1129 'showalldb' => false,
1130 'numberingroup' => false,
1131 'inlanguagecode' => null,
1135 protected function getExamplesMessages() {
1136 return [
1137 'action=query&meta=siteinfo&siprop=general|namespaces|namespacealiases|statistics'
1138 => 'apihelp-query+siteinfo-example-simple',
1139 'action=query&meta=siteinfo&siprop=interwikimap&sifilteriw=local'
1140 => 'apihelp-query+siteinfo-example-interwiki',
1141 'action=query&meta=siteinfo&siprop=dbrepllag&sishowalldb='
1142 => 'apihelp-query+siteinfo-example-replag',
1146 public function getHelpUrls() {
1147 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Siteinfo';
1151 /** @deprecated class alias since 1.43 */
1152 class_alias( ApiQuerySiteinfo::class, 'ApiQuerySiteinfo' );