Update git submodules
[mediawiki.git] / includes / api / ApiQuerySiteinfo.php
blob49598e028cd228e475975f7e9c6cd14c746b5b1a
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 use MediaWiki\Interwiki\InterwikiLookup;
24 use MediaWiki\Languages\LanguageConverterFactory;
25 use MediaWiki\Languages\LanguageFactory;
26 use MediaWiki\Languages\LanguageNameUtils;
27 use MediaWiki\MainConfigNames;
28 use MediaWiki\MediaWikiServices;
29 use MediaWiki\Parser\MagicWordFactory;
30 use MediaWiki\ResourceLoader\SkinModule;
31 use MediaWiki\SiteStats\SiteStats;
32 use MediaWiki\SpecialPage\SpecialPage;
33 use MediaWiki\SpecialPage\SpecialPageFactory;
34 use MediaWiki\Specials\SpecialVersion;
35 use MediaWiki\Title\NamespaceInfo;
36 use MediaWiki\Title\Title;
37 use MediaWiki\User\UserGroupManager;
38 use MediaWiki\User\UserOptionsLookup;
39 use MediaWiki\Utils\ExtensionInfo;
40 use MediaWiki\Utils\GitInfo;
41 use MediaWiki\Utils\UrlUtils;
42 use MediaWiki\WikiMap\WikiMap;
43 use Wikimedia\ParamValidator\ParamValidator;
44 use Wikimedia\Rdbms\ILoadBalancer;
45 use Wikimedia\Rdbms\ReadOnlyMode;
47 /**
48 * A query action to return meta information about the wiki site.
50 * @ingroup API
52 class ApiQuerySiteinfo extends ApiQueryBase {
54 private UserOptionsLookup $userOptionsLookup;
55 private UserGroupManager $userGroupManager;
56 private LanguageConverterFactory $languageConverterFactory;
57 private LanguageFactory $languageFactory;
58 private LanguageNameUtils $languageNameUtils;
59 private Language $contentLanguage;
60 private NamespaceInfo $namespaceInfo;
61 private InterwikiLookup $interwikiLookup;
62 private ParserFactory $parserFactory;
63 private MagicWordFactory $magicWordFactory;
64 private SpecialPageFactory $specialPageFactory;
65 private SkinFactory $skinFactory;
66 private ILoadBalancer $loadBalancer;
67 private ReadOnlyMode $readOnlyMode;
68 private UrlUtils $urlUtils;
70 /**
71 * @param ApiQuery $query
72 * @param string $moduleName
73 * @param UserOptionsLookup $userOptionsLookup
74 * @param UserGroupManager $userGroupManager
75 * @param LanguageConverterFactory $languageConverterFactory
76 * @param LanguageFactory $languageFactory
77 * @param LanguageNameUtils $languageNameUtils
78 * @param Language $contentLanguage
79 * @param NamespaceInfo $namespaceInfo
80 * @param InterwikiLookup $interwikiLookup
81 * @param ParserFactory $parserFactory
82 * @param MagicWordFactory $magicWordFactory
83 * @param SpecialPageFactory $specialPageFactory
84 * @param SkinFactory $skinFactory
85 * @param ILoadBalancer $loadBalancer
86 * @param ReadOnlyMode $readOnlyMode
87 * @param UrlUtils $urlUtils
89 public function __construct(
90 ApiQuery $query,
91 $moduleName,
92 UserOptionsLookup $userOptionsLookup,
93 UserGroupManager $userGroupManager,
94 LanguageConverterFactory $languageConverterFactory,
95 LanguageFactory $languageFactory,
96 LanguageNameUtils $languageNameUtils,
97 Language $contentLanguage,
98 NamespaceInfo $namespaceInfo,
99 InterwikiLookup $interwikiLookup,
100 ParserFactory $parserFactory,
101 MagicWordFactory $magicWordFactory,
102 SpecialPageFactory $specialPageFactory,
103 SkinFactory $skinFactory,
104 ILoadBalancer $loadBalancer,
105 ReadOnlyMode $readOnlyMode,
106 UrlUtils $urlUtils
108 parent::__construct( $query, $moduleName, 'si' );
109 $this->userOptionsLookup = $userOptionsLookup;
110 $this->userGroupManager = $userGroupManager;
111 $this->languageConverterFactory = $languageConverterFactory;
112 $this->languageFactory = $languageFactory;
113 $this->languageNameUtils = $languageNameUtils;
114 $this->contentLanguage = $contentLanguage;
115 $this->namespaceInfo = $namespaceInfo;
116 $this->interwikiLookup = $interwikiLookup;
117 $this->parserFactory = $parserFactory;
118 $this->magicWordFactory = $magicWordFactory;
119 $this->specialPageFactory = $specialPageFactory;
120 $this->skinFactory = $skinFactory;
121 $this->loadBalancer = $loadBalancer;
122 $this->readOnlyMode = $readOnlyMode;
123 $this->urlUtils = $urlUtils;
126 public function execute() {
127 $params = $this->extractRequestParams();
128 $done = [];
129 foreach ( $params['prop'] as $p ) {
130 switch ( $p ) {
131 case 'general':
132 $fit = $this->appendGeneralInfo( $p );
133 break;
134 case 'namespaces':
135 $fit = $this->appendNamespaces( $p );
136 break;
137 case 'namespacealiases':
138 $fit = $this->appendNamespaceAliases( $p );
139 break;
140 case 'specialpagealiases':
141 $fit = $this->appendSpecialPageAliases( $p );
142 break;
143 case 'magicwords':
144 $fit = $this->appendMagicWords( $p );
145 break;
146 case 'interwikimap':
147 $fit = $this->appendInterwikiMap( $p, $params['filteriw'] );
148 break;
149 case 'dbrepllag':
150 $fit = $this->appendDbReplLagInfo( $p, $params['showalldb'] );
151 break;
152 case 'statistics':
153 $fit = $this->appendStatistics( $p );
154 break;
155 case 'usergroups':
156 $fit = $this->appendUserGroups( $p, $params['numberingroup'] );
157 break;
158 case 'autocreatetempuser':
159 $fit = $this->appendAutoCreateTempUser( $p );
160 break;
161 case 'libraries':
162 $fit = $this->appendInstalledLibraries( $p );
163 break;
164 case 'extensions':
165 $fit = $this->appendExtensions( $p );
166 break;
167 case 'fileextensions':
168 $fit = $this->appendFileExtensions( $p );
169 break;
170 case 'rightsinfo':
171 $fit = $this->appendRightsInfo( $p );
172 break;
173 case 'restrictions':
174 $fit = $this->appendRestrictions( $p );
175 break;
176 case 'languages':
177 $fit = $this->appendLanguages( $p );
178 break;
179 case 'languagevariants':
180 $fit = $this->appendLanguageVariants( $p );
181 break;
182 case 'skins':
183 $fit = $this->appendSkins( $p );
184 break;
185 case 'extensiontags':
186 $fit = $this->appendExtensionTags( $p );
187 break;
188 case 'functionhooks':
189 $fit = $this->appendFunctionHooks( $p );
190 break;
191 case 'showhooks':
192 $fit = $this->appendSubscribedHooks( $p );
193 break;
194 case 'variables':
195 $fit = $this->appendVariables( $p );
196 break;
197 case 'protocols':
198 $fit = $this->appendProtocols( $p );
199 break;
200 case 'defaultoptions':
201 $fit = $this->appendDefaultOptions( $p );
202 break;
203 case 'uploaddialog':
204 $fit = $this->appendUploadDialog( $p );
205 break;
206 default:
207 ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" ); // @codeCoverageIgnore
209 if ( !$fit ) {
210 // Abuse siprop as a query-continue parameter
211 // and set it to all unprocessed props
212 $this->setContinueEnumParameter( 'prop', implode( '|',
213 array_diff( $params['prop'], $done ) ) );
214 break;
216 $done[] = $p;
220 protected function appendGeneralInfo( $property ) {
221 $config = $this->getConfig();
223 $data = [];
224 $mainPage = Title::newMainPage();
225 $data['mainpage'] = $mainPage->getPrefixedText();
226 $data['base'] = (string)$this->urlUtils->expand( $mainPage->getFullURL(), PROTO_CURRENT );
227 $data['sitename'] = $config->get( MainConfigNames::Sitename );
228 $data['mainpageisdomainroot'] = (bool)$config->get( MainConfigNames::MainPageIsDomainRoot );
230 // A logo can either be a relative or an absolute path
231 // make sure we always return an absolute path
232 $logo = SkinModule::getAvailableLogos( $config );
233 $data['logo'] = (string)$this->urlUtils->expand( $logo['1x'], PROTO_RELATIVE );
235 $data['generator'] = 'MediaWiki ' . MW_VERSION;
237 $data['phpversion'] = PHP_VERSION;
238 $data['phpsapi'] = PHP_SAPI;
239 $data['dbtype'] = $config->get( MainConfigNames::DBtype );
240 $data['dbversion'] = $this->getDB()->getServerVersion();
242 $allowFrom = [ '' ];
243 $allowException = true;
244 if ( !$config->get( MainConfigNames::AllowExternalImages ) ) {
245 $data['imagewhitelistenabled'] =
246 (bool)$config->get( MainConfigNames::EnableImageWhitelist );
247 $allowFrom = $config->get( MainConfigNames::AllowExternalImagesFrom );
248 $allowException = (bool)$allowFrom;
250 if ( $allowException ) {
251 $data['externalimages'] = (array)$allowFrom;
252 ApiResult::setIndexedTagName( $data['externalimages'], 'prefix' );
255 $data['langconversion'] = !$this->languageConverterFactory->isConversionDisabled();
256 $data['linkconversion'] = !$this->languageConverterFactory->isLinkConversionDisabled();
257 // For backwards compatibility (soft deprecated since MW 1.36)
258 $data['titleconversion'] = $data['linkconversion'];
260 $contLangConverter = $this->languageConverterFactory->getLanguageConverter( $this->contentLanguage );
261 if ( $this->contentLanguage->linkPrefixExtension() ) {
262 $linkPrefixCharset = $this->contentLanguage->linkPrefixCharset();
263 $data['linkprefixcharset'] = $linkPrefixCharset;
264 // For backwards compatibility
265 $data['linkprefix'] = "/^((?>.*[^$linkPrefixCharset]|))(.+)$/sDu";
266 } else {
267 $data['linkprefixcharset'] = '';
268 $data['linkprefix'] = '';
271 $linktrail = $this->contentLanguage->linkTrail();
272 $data['linktrail'] = $linktrail ?: '';
274 $data['legaltitlechars'] = Title::legalChars();
275 $data['invalidusernamechars'] = $config->get( MainConfigNames::InvalidUsernameCharacters );
277 $data['allunicodefixes'] = (bool)$config->get( MainConfigNames::AllUnicodeFixes );
278 $data['fixarabicunicode'] = true; // Config removed in 1.35, always true
279 $data['fixmalayalamunicode'] = true; // Config removed in 1.35, always true
281 $baseDir = $this->getConfig()->get( MainConfigNames::BaseDirectory );
282 $git = SpecialVersion::getGitHeadSha1( $baseDir );
283 if ( $git ) {
284 $data['git-hash'] = $git;
285 $data['git-branch'] =
286 SpecialVersion::getGitCurrentBranch( $baseDir );
289 // 'case-insensitive' option is reserved for future
290 $data['case'] =
291 $config->get( MainConfigNames::CapitalLinks ) ? 'first-letter' : 'case-sensitive';
292 $data['lang'] = $config->get( MainConfigNames::LanguageCode );
294 $fallbacks = [];
295 foreach ( $this->contentLanguage->getFallbackLanguages() as $code ) {
296 $fallbacks[] = [ 'code' => $code ];
298 $data['fallback'] = $fallbacks;
299 ApiResult::setIndexedTagName( $data['fallback'], 'lang' );
301 if ( $contLangConverter->hasVariants() ) {
302 $variants = [];
303 foreach ( $contLangConverter->getVariants() as $code ) {
304 $variants[] = [
305 'code' => $code,
306 'name' => $this->contentLanguage->getVariantname( $code ),
309 $data['variants'] = $variants;
310 ApiResult::setIndexedTagName( $data['variants'], 'lang' );
313 $data['rtl'] = $this->contentLanguage->isRTL();
314 $data['fallback8bitEncoding'] = $this->contentLanguage->fallback8bitEncoding();
316 $data['readonly'] = $this->readOnlyMode->isReadOnly();
317 if ( $data['readonly'] ) {
318 $data['readonlyreason'] = $this->readOnlyMode->getReason();
320 $data['writeapi'] = true; // Deprecated since MW 1.32
322 $data['maxarticlesize'] = $config->get( MainConfigNames::MaxArticleSize ) * 1024;
324 $tz = $config->get( MainConfigNames::Localtimezone );
325 $offset = $config->get( MainConfigNames::LocalTZoffset );
326 $data['timezone'] = $tz;
327 $data['timeoffset'] = (int)$offset;
328 $data['articlepath'] = $config->get( MainConfigNames::ArticlePath );
329 $data['scriptpath'] = $config->get( MainConfigNames::ScriptPath );
330 $data['script'] = $config->get( MainConfigNames::Script );
331 $data['variantarticlepath'] = $config->get( MainConfigNames::VariantArticlePath );
332 $data[ApiResult::META_BC_BOOLS][] = 'variantarticlepath';
333 $data['server'] = $config->get( MainConfigNames::Server );
334 $data['servername'] = $config->get( MainConfigNames::ServerName );
335 $data['wikiid'] = WikiMap::getCurrentWikiId();
336 $data['time'] = wfTimestamp( TS_ISO_8601, time() );
338 $data['misermode'] = (bool)$config->get( MainConfigNames::MiserMode );
340 $data['uploadsenabled'] = UploadBase::isEnabled();
341 $data['maxuploadsize'] = UploadBase::getMaxUploadSize();
342 $data['minuploadchunksize'] = ApiUpload::getMinUploadChunkSize( $config );
344 $data['galleryoptions'] = $config->get( MainConfigNames::GalleryOptions );
346 $data['thumblimits'] = $config->get( MainConfigNames::ThumbLimits );
347 ApiResult::setArrayType( $data['thumblimits'], 'BCassoc' );
348 ApiResult::setIndexedTagName( $data['thumblimits'], 'limit' );
349 $data['imagelimits'] = [];
350 ApiResult::setArrayType( $data['imagelimits'], 'BCassoc' );
351 ApiResult::setIndexedTagName( $data['imagelimits'], 'limit' );
352 foreach ( $config->get( MainConfigNames::ImageLimits ) as $k => $limit ) {
353 $data['imagelimits'][$k] = [ 'width' => $limit[0], 'height' => $limit[1] ];
356 $favicon = $config->get( MainConfigNames::Favicon );
357 if ( $favicon ) {
358 // Expand any local path to full URL to improve API usability (T77093).
359 $data['favicon'] = (string)$this->urlUtils->expand( $favicon );
362 $data['centralidlookupprovider'] =
363 $config->get( MainConfigNames::CentralIdLookupProvider );
364 $providerIds = array_keys( $config->get( MainConfigNames::CentralIdLookupProviders ) );
365 $data['allcentralidlookupproviders'] = $providerIds;
367 $data['interwikimagic'] = (bool)$config->get( MainConfigNames::InterwikiMagic );
368 $data['magiclinks'] = $config->get( MainConfigNames::EnableMagicLinks );
370 $data['categorycollation'] = $config->get( MainConfigNames::CategoryCollation );
372 $data['nofollowlinks'] = $config->get( MainConfigNames::NoFollowLinks );
373 $data['nofollownsexceptions'] = $config->get( MainConfigNames::NoFollowNsExceptions );
374 $data['nofollowdomainexceptions'] = $config->get( MainConfigNames::NoFollowDomainExceptions );
375 $data['externallinktarget'] = $config->get( MainConfigNames::ExternalLinkTarget );
377 $this->getHookRunner()->onAPIQuerySiteInfoGeneralInfo( $this, $data );
379 return $this->getResult()->addValue( 'query', $property, $data );
382 protected function appendNamespaces( $property ) {
383 $nsProtection = $this->getConfig()->get( MainConfigNames::NamespaceProtection );
385 $data = [
386 ApiResult::META_TYPE => 'assoc',
388 foreach (
389 $this->contentLanguage->getFormattedNamespaces()
390 as $ns => $title
392 $data[$ns] = [
393 'id' => (int)$ns,
394 'case' => $this->namespaceInfo->isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
396 ApiResult::setContentValue( $data[$ns], 'name', $title );
397 $canonical = $this->namespaceInfo->getCanonicalName( $ns );
399 $data[$ns]['subpages'] = $this->namespaceInfo->hasSubpages( $ns );
401 if ( $canonical ) {
402 $data[$ns]['canonical'] = strtr( $canonical, '_', ' ' );
405 $data[$ns]['content'] = $this->namespaceInfo->isContent( $ns );
406 $data[$ns]['nonincludable'] = $this->namespaceInfo->isNonincludable( $ns );
408 if ( isset( $nsProtection[$ns] ) ) {
409 if ( is_array( $nsProtection[$ns] ) ) {
410 $specificNs = implode( "|", array_filter( $nsProtection[$ns] ) );
411 } elseif ( $nsProtection[$ns] !== '' ) {
412 $specificNs = $nsProtection[$ns];
414 if ( isset( $specificNs ) && $specificNs !== '' ) {
415 $data[$ns]['namespaceprotection'] = $specificNs;
419 $contentmodel = $this->namespaceInfo->getNamespaceContentModel( $ns );
420 if ( $contentmodel ) {
421 $data[$ns]['defaultcontentmodel'] = $contentmodel;
425 ApiResult::setArrayType( $data, 'assoc' );
426 ApiResult::setIndexedTagName( $data, 'ns' );
428 return $this->getResult()->addValue( 'query', $property, $data );
431 protected function appendNamespaceAliases( $property ) {
432 $aliases = $this->contentLanguage->getNamespaceAliases();
433 $namespaces = $this->contentLanguage->getNamespaces();
434 $data = [];
435 foreach ( $aliases as $title => $ns ) {
436 if ( $namespaces[$ns] == $title ) {
437 // Don't list duplicates
438 continue;
440 $item = [
441 'id' => (int)$ns
443 ApiResult::setContentValue( $item, 'alias', strtr( $title, '_', ' ' ) );
444 $data[] = $item;
447 sort( $data );
449 ApiResult::setIndexedTagName( $data, 'ns' );
451 return $this->getResult()->addValue( 'query', $property, $data );
454 protected function appendSpecialPageAliases( $property ) {
455 $data = [];
456 $aliases = $this->contentLanguage->getSpecialPageAliases();
457 foreach ( $this->specialPageFactory->getNames() as $specialpage ) {
458 if ( isset( $aliases[$specialpage] ) ) {
459 $arr = [ 'realname' => $specialpage, 'aliases' => $aliases[$specialpage] ];
460 ApiResult::setIndexedTagName( $arr['aliases'], 'alias' );
461 $data[] = $arr;
464 ApiResult::setIndexedTagName( $data, 'specialpage' );
466 return $this->getResult()->addValue( 'query', $property, $data );
469 protected function appendMagicWords( $property ) {
470 $data = [];
471 foreach (
472 $this->contentLanguage->getMagicWords()
473 as $magicword => $aliases
475 $caseSensitive = array_shift( $aliases );
476 $arr = [ 'name' => $magicword, 'aliases' => $aliases ];
477 $arr['case-sensitive'] = (bool)$caseSensitive;
478 ApiResult::setIndexedTagName( $arr['aliases'], 'alias' );
479 $data[] = $arr;
481 ApiResult::setIndexedTagName( $data, 'magicword' );
483 return $this->getResult()->addValue( 'query', $property, $data );
486 protected function appendInterwikiMap( $property, $filter ) {
487 if ( $filter === 'local' ) {
488 $local = true;
489 } elseif ( $filter === '!local' ) {
490 $local = false;
491 } else {
492 // $filter === null
493 $local = null;
496 $params = $this->extractRequestParams();
497 $langCode = $params['inlanguagecode'] ?? '';
498 $interwikiMagic = $this->getConfig()->get( MainConfigNames::InterwikiMagic );
500 if ( $interwikiMagic ) {
501 $langNames = $this->languageNameUtils->getLanguageNames( $langCode );
504 $getPrefixes = $this->interwikiLookup->getAllPrefixes( $local );
505 $extraLangPrefixes = $this->getConfig()->get( MainConfigNames::ExtraInterlanguageLinkPrefixes );
506 $extraLangCodeMap = $this->getConfig()->get( MainConfigNames::InterlanguageLinkCodeMap );
507 $localInterwikis = $this->getConfig()->get( MainConfigNames::LocalInterwikis );
508 $data = [];
510 foreach ( $getPrefixes as $row ) {
511 $prefix = $row['iw_prefix'];
512 $val = [];
513 $val['prefix'] = $prefix;
514 if ( isset( $row['iw_local'] ) && $row['iw_local'] == '1' ) {
515 $val['local'] = true;
517 if ( isset( $row['iw_trans'] ) && $row['iw_trans'] == '1' ) {
518 $val['trans'] = true;
521 if ( $interwikiMagic && isset( $langNames[$prefix] ) ) {
522 $val['language'] = $langNames[$prefix];
523 $standard = LanguageCode::replaceDeprecatedCodes( $prefix );
524 if ( $standard !== $prefix ) {
525 # Note that even if this code is deprecated, it should
526 # only be remapped if extralanglink (set below) is false.
527 $val['deprecated'] = $standard;
529 $val['bcp47'] = LanguageCode::bcp47( $standard );
531 if ( in_array( $prefix, $localInterwikis ) ) {
532 $val['localinterwiki'] = true;
534 if ( $interwikiMagic && in_array( $prefix, $extraLangPrefixes ) ) {
535 $val['extralanglink'] = true;
536 $val['code'] = $extraLangCodeMap[$prefix] ?? $prefix;
537 $val['bcp47'] = LanguageCode::bcp47( $val['code'] );
539 $linktext = $this->msg( "interlanguage-link-$prefix" );
540 if ( !$linktext->isDisabled() ) {
541 $val['linktext'] = $linktext->text();
544 $sitename = $this->msg( "interlanguage-link-sitename-$prefix" );
545 if ( !$sitename->isDisabled() ) {
546 $val['sitename'] = $sitename->text();
550 $val['url'] = (string)$this->urlUtils->expand( $row['iw_url'], PROTO_CURRENT );
551 $val['protorel'] = str_starts_with( $row['iw_url'], '//' );
552 if ( isset( $row['iw_wikiid'] ) && $row['iw_wikiid'] !== '' ) {
553 $val['wikiid'] = $row['iw_wikiid'];
555 if ( isset( $row['iw_api'] ) && $row['iw_api'] !== '' ) {
556 $val['api'] = $row['iw_api'];
559 $data[] = $val;
562 ApiResult::setIndexedTagName( $data, 'iw' );
564 return $this->getResult()->addValue( 'query', $property, $data );
567 protected function appendDbReplLagInfo( $property, $includeAll ) {
568 $data = [];
569 $showHostnames = $this->getConfig()->get( MainConfigNames::ShowHostnames );
570 if ( $includeAll ) {
571 if ( !$showHostnames ) {
572 $this->dieWithError( 'apierror-siteinfo-includealldenied', 'includeAllDenied' );
575 $lags = $this->loadBalancer->getLagTimes();
576 foreach ( $lags as $i => $lag ) {
577 $data[] = [
578 'host' => $this->loadBalancer->getServerName( $i ),
579 'lag' => $lag
582 } else {
583 [ , $lag, $index ] = $this->loadBalancer->getMaxLag();
584 $data[] = [
585 'host' => $showHostnames
586 ? $this->loadBalancer->getServerName( $index )
587 : '',
588 'lag' => $lag
592 ApiResult::setIndexedTagName( $data, 'db' );
594 return $this->getResult()->addValue( 'query', $property, $data );
597 protected function appendStatistics( $property ) {
598 $data = [];
599 $data['pages'] = (int)SiteStats::pages();
600 $data['articles'] = (int)SiteStats::articles();
601 $data['edits'] = (int)SiteStats::edits();
602 $data['images'] = (int)SiteStats::images();
603 $data['users'] = (int)SiteStats::users();
604 $data['activeusers'] = (int)SiteStats::activeUsers();
605 $data['admins'] = (int)SiteStats::numberingroup( 'sysop' );
606 $data['jobs'] = (int)SiteStats::jobs();
608 $this->getHookRunner()->onAPIQuerySiteInfoStatisticsInfo( $data );
610 return $this->getResult()->addValue( 'query', $property, $data );
613 protected function appendUserGroups( $property, $numberInGroup ) {
614 $config = $this->getConfig();
616 $data = [];
617 $result = $this->getResult();
618 $allGroups = array_values( $this->userGroupManager->listAllGroups() );
619 foreach ( $config->get( MainConfigNames::GroupPermissions ) as $group => $permissions ) {
620 $arr = [
621 'name' => $group,
622 'rights' => array_keys( $permissions, true ),
625 if ( $numberInGroup ) {
626 $autopromote = $config->get( MainConfigNames::Autopromote );
628 if ( $group == 'user' ) {
629 $arr['number'] = SiteStats::users();
630 // '*' and autopromote groups have no size
631 } elseif ( $group !== '*' && !isset( $autopromote[$group] ) ) {
632 $arr['number'] = SiteStats::numberingroup( $group );
636 $groupArr = [
637 'add' => $config->get( MainConfigNames::AddGroups ),
638 'remove' => $config->get( MainConfigNames::RemoveGroups ),
639 'add-self' => $config->get( MainConfigNames::GroupsAddToSelf ),
640 'remove-self' => $config->get( MainConfigNames::GroupsRemoveFromSelf )
643 foreach ( $groupArr as $type => $rights ) {
644 if ( isset( $rights[$group] ) ) {
645 if ( $rights[$group] === true ) {
646 $groups = $allGroups;
647 } else {
648 $groups = array_intersect( $rights[$group], $allGroups );
650 if ( $groups ) {
651 $arr[$type] = $groups;
652 ApiResult::setArrayType( $arr[$type], 'BCarray' );
653 ApiResult::setIndexedTagName( $arr[$type], 'group' );
658 ApiResult::setIndexedTagName( $arr['rights'], 'permission' );
659 $data[] = $arr;
662 ApiResult::setIndexedTagName( $data, 'group' );
664 return $result->addValue( 'query', $property, $data );
667 protected function appendAutoCreateTempUser( $property ) {
668 $config = $this->getConfig()->get( MainConfigNames::AutoCreateTempUser );
670 $data = [ 'enabled' => false ];
671 if ( $config['enabled'] ?? false ) {
672 $data['enabled'] = true;
673 $data['actions'] = $config['actions'];
674 $data['genPattern'] = $config['genPattern'];
675 $data['matchPattern'] = $config['matchPattern'] ?? $data['genPattern'];
676 $data['serialProvider'] = $config['serialProvider'];
677 $data['serialMapping'] = $config['serialMapping'];
679 if ( isset( $config['reservedPattern'] ) ) {
680 $data['reservedPattern'] = $config['reservedPattern'];
683 return $this->getResult()->addValue( 'query', $property, $data );
686 protected function appendFileExtensions( $property ) {
687 $data = [];
688 foreach (
689 array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) ) as $ext
691 $data[] = [ 'ext' => $ext ];
693 ApiResult::setIndexedTagName( $data, 'fe' );
695 return $this->getResult()->addValue( 'query', $property, $data );
698 protected function appendInstalledLibraries( $property ) {
699 $baseDir = $this->getConfig()->get( MainConfigNames::BaseDirectory );
700 $path = "$baseDir/vendor/composer/installed.json";
701 if ( !file_exists( $path ) ) {
702 return true;
705 $data = [];
706 $installed = new ComposerInstalled( $path );
707 foreach ( $installed->getInstalledDependencies() as $name => $info ) {
708 if ( str_starts_with( $info['type'], 'mediawiki-' ) ) {
709 // Skip any extensions or skins since they'll be listed
710 // in their proper section
711 continue;
713 $data[] = [
714 'name' => $name,
715 'version' => $info['version'],
718 ApiResult::setIndexedTagName( $data, 'library' );
720 return $this->getResult()->addValue( 'query', $property, $data );
723 protected function appendExtensions( $property ) {
724 $data = [];
725 $credits = SpecialVersion::getCredits(
726 ExtensionRegistry::getInstance(),
727 $this->getConfig()
729 foreach ( $credits as $type => $extensions ) {
730 foreach ( $extensions as $ext ) {
731 $ret = [];
732 $ret['type'] = $type;
733 if ( isset( $ext['name'] ) ) {
734 $ret['name'] = $ext['name'];
736 if ( isset( $ext['namemsg'] ) ) {
737 $ret['namemsg'] = $ext['namemsg'];
739 if ( isset( $ext['description'] ) ) {
740 $ret['description'] = $ext['description'];
742 if ( isset( $ext['descriptionmsg'] ) ) {
743 // Can be a string or [ key, param1, param2, ... ]
744 if ( is_array( $ext['descriptionmsg'] ) ) {
745 $ret['descriptionmsg'] = $ext['descriptionmsg'][0];
746 $ret['descriptionmsgparams'] = array_slice( $ext['descriptionmsg'], 1 );
747 ApiResult::setIndexedTagName( $ret['descriptionmsgparams'], 'param' );
748 } else {
749 $ret['descriptionmsg'] = $ext['descriptionmsg'];
752 if ( isset( $ext['author'] ) ) {
753 $ret['author'] = is_array( $ext['author'] ) ?
754 implode( ', ', $ext['author'] ) : $ext['author'];
756 if ( isset( $ext['url'] ) ) {
757 $ret['url'] = $ext['url'];
759 if ( isset( $ext['version'] ) ) {
760 $ret['version'] = $ext['version'];
762 if ( isset( $ext['path'] ) ) {
763 $extensionPath = dirname( $ext['path'] );
764 $gitInfo = new GitInfo( $extensionPath );
765 $vcsVersion = $gitInfo->getHeadSHA1();
766 if ( $vcsVersion !== false ) {
767 $ret['vcs-system'] = 'git';
768 $ret['vcs-version'] = $vcsVersion;
769 $ret['vcs-url'] = $gitInfo->getHeadViewUrl();
770 $vcsDate = $gitInfo->getHeadCommitDate();
771 if ( $vcsDate !== false ) {
772 $ret['vcs-date'] = wfTimestamp( TS_ISO_8601, $vcsDate );
776 if ( ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
777 $ret['license-name'] = $ext['license-name'] ?? '';
778 $ret['license'] = SpecialPage::getTitleFor(
779 'Version',
780 "License/{$ext['name']}"
781 )->getLinkURL();
784 if ( ExtensionInfo::getAuthorsFileName( $extensionPath ) ) {
785 $ret['credits'] = SpecialPage::getTitleFor(
786 'Version',
787 "Credits/{$ext['name']}"
788 )->getLinkURL();
791 $data[] = $ret;
795 ApiResult::setIndexedTagName( $data, 'ext' );
797 return $this->getResult()->addValue( 'query', $property, $data );
800 protected function appendRightsInfo( $property ) {
801 $config = $this->getConfig();
802 $rightsPage = $config->get( MainConfigNames::RightsPage );
803 // The default value is null, but the installer sets it to empty string
804 if ( strlen( (string)$rightsPage ) ) {
805 $title = Title::newFromText( $rightsPage );
806 $url = (string)$this->urlUtils->expand( $title->getLinkURL(), PROTO_CURRENT );
807 } else {
808 $title = false;
809 $url = $config->get( MainConfigNames::RightsUrl );
811 $text = $config->get( MainConfigNames::RightsText );
812 if ( $title && !strlen( (string)$text ) ) {
813 $text = $title->getPrefixedText();
816 $data = [
817 'url' => (string)$url,
818 'text' => (string)$text,
821 return $this->getResult()->addValue( 'query', $property, $data );
824 protected function appendRestrictions( $property ) {
825 $config = $this->getConfig();
826 $data = [
827 'types' => $config->get( MainConfigNames::RestrictionTypes ),
828 'levels' => $config->get( MainConfigNames::RestrictionLevels ),
829 'cascadinglevels' => $config->get( MainConfigNames::CascadingRestrictionLevels ),
830 'semiprotectedlevels' => $config->get( MainConfigNames::SemiprotectedRestrictionLevels ),
833 ApiResult::setArrayType( $data['types'], 'BCarray' );
834 ApiResult::setArrayType( $data['levels'], 'BCarray' );
835 ApiResult::setArrayType( $data['cascadinglevels'], 'BCarray' );
836 ApiResult::setArrayType( $data['semiprotectedlevels'], 'BCarray' );
838 ApiResult::setIndexedTagName( $data['types'], 'type' );
839 ApiResult::setIndexedTagName( $data['levels'], 'level' );
840 ApiResult::setIndexedTagName( $data['cascadinglevels'], 'level' );
841 ApiResult::setIndexedTagName( $data['semiprotectedlevels'], 'level' );
843 return $this->getResult()->addValue( 'query', $property, $data );
846 public function appendLanguages( $property ) {
847 $params = $this->extractRequestParams();
848 $langCode = $params['inlanguagecode'] ?? '';
849 $langNames = $this->languageNameUtils->getLanguageNames( $langCode );
851 $data = [];
853 foreach ( $langNames as $code => $name ) {
854 $lang = [
855 'code' => $code,
856 'bcp47' => LanguageCode::bcp47( $code ),
858 ApiResult::setContentValue( $lang, 'name', $name );
859 $data[] = $lang;
861 ApiResult::setIndexedTagName( $data, 'lang' );
863 return $this->getResult()->addValue( 'query', $property, $data );
866 // Export information about which page languages will trigger
867 // language conversion. (T153341)
868 public function appendLanguageVariants( $property ) {
869 $langNames = LanguageConverter::$languagesWithVariants;
870 if ( $this->languageConverterFactory->isConversionDisabled() ) {
871 // Ensure result is empty if language conversion is disabled.
872 $langNames = [];
874 sort( $langNames );
876 $data = [];
877 foreach ( $langNames as $langCode ) {
878 $lang = $this->languageFactory->getLanguage( $langCode );
879 $langConverter = $this->languageConverterFactory->getLanguageConverter( $lang );
880 if ( !$langConverter->hasVariants() ) {
881 // Only languages which have variants should be listed
882 continue;
884 $data[$langCode] = [];
885 ApiResult::setIndexedTagName( $data[$langCode], 'variant' );
886 ApiResult::setArrayType( $data[$langCode], 'kvp', 'code' );
888 $variants = $langConverter->getVariants();
889 sort( $variants );
890 foreach ( $variants as $v ) {
891 $fallbacks = $langConverter->getVariantFallbacks( $v );
892 if ( !is_array( $fallbacks ) ) {
893 $fallbacks = [ $fallbacks ];
895 $data[$langCode][$v] = [
896 'fallbacks' => $fallbacks,
898 ApiResult::setIndexedTagName(
899 $data[$langCode][$v]['fallbacks'], 'variant'
903 ApiResult::setIndexedTagName( $data, 'lang' );
904 ApiResult::setArrayType( $data, 'kvp', 'code' );
906 return $this->getResult()->addValue( 'query', $property, $data );
909 public function appendSkins( $property ) {
910 $data = [];
911 $allowed = $this->skinFactory->getAllowedSkins();
912 $default = Skin::normalizeKey( 'default' );
913 $skinNames = $this->skinFactory->getInstalledSkins();
915 foreach ( $skinNames as $name => $displayName ) {
916 $msg = $this->msg( "skinname-{$name}" );
917 $code = $this->getParameter( 'inlanguagecode' );
918 if ( $code && $this->languageNameUtils->isValidCode( $code ) ) {
919 $msg->inLanguage( $code );
920 } else {
921 $msg->inContentLanguage();
923 if ( $msg->exists() ) {
924 $displayName = $msg->text();
926 $skin = [ 'code' => $name ];
927 ApiResult::setContentValue( $skin, 'name', $displayName );
928 if ( !isset( $allowed[$name] ) ) {
929 $skin['unusable'] = true;
931 if ( $name === $default ) {
932 $skin['default'] = true;
934 $data[] = $skin;
936 ApiResult::setIndexedTagName( $data, 'skin' );
938 return $this->getResult()->addValue( 'query', $property, $data );
941 public function appendExtensionTags( $property ) {
942 $tags = array_map(
943 static function ( $item ) {
944 return "<$item>";
946 $this->parserFactory->getMainInstance()->getTags()
948 ApiResult::setArrayType( $tags, 'BCarray' );
949 ApiResult::setIndexedTagName( $tags, 't' );
951 return $this->getResult()->addValue( 'query', $property, $tags );
954 public function appendFunctionHooks( $property ) {
955 $hooks = $this->parserFactory->getMainInstance()->getFunctionHooks();
956 ApiResult::setArrayType( $hooks, 'BCarray' );
957 ApiResult::setIndexedTagName( $hooks, 'h' );
959 return $this->getResult()->addValue( 'query', $property, $hooks );
962 public function appendVariables( $property ) {
963 $variables = $this->magicWordFactory->getVariableIDs();
964 ApiResult::setArrayType( $variables, 'BCarray' );
965 ApiResult::setIndexedTagName( $variables, 'v' );
967 return $this->getResult()->addValue( 'query', $property, $variables );
970 public function appendProtocols( $property ) {
971 // Make a copy of the global so we don't try to set the _element key of it - T47130
972 $protocols = array_values( $this->getConfig()->get( MainConfigNames::UrlProtocols ) );
973 ApiResult::setArrayType( $protocols, 'BCarray' );
974 ApiResult::setIndexedTagName( $protocols, 'p' );
976 return $this->getResult()->addValue( 'query', $property, $protocols );
979 public function appendDefaultOptions( $property ) {
980 $options = $this->userOptionsLookup->getDefaultOptions();
981 $options[ApiResult::META_BC_BOOLS] = array_keys( $options );
982 return $this->getResult()->addValue( 'query', $property, $options );
985 public function appendUploadDialog( $property ) {
986 $config = $this->getConfig()->get( MainConfigNames::UploadDialog );
987 return $this->getResult()->addValue( 'query', $property, $config );
990 public function appendSubscribedHooks( $property ) {
991 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
992 $hookNames = $hookContainer->getHookNames();
993 sort( $hookNames );
995 $data = [];
996 foreach ( $hookNames as $name ) {
997 $subscribers = $hookContainer->getHandlerDescriptions( $name );
999 $arr = [
1000 'name' => $name,
1001 'subscribers' => $subscribers,
1004 ApiResult::setArrayType( $arr['subscribers'], 'array' );
1005 ApiResult::setIndexedTagName( $arr['subscribers'], 's' );
1006 $data[] = $arr;
1009 ApiResult::setIndexedTagName( $data, 'hook' );
1011 return $this->getResult()->addValue( 'query', $property, $data );
1014 public function getCacheMode( $params ) {
1015 // Messages for $wgExtraInterlanguageLinkPrefixes depend on user language
1016 if (
1017 count( $this->getConfig()->get( MainConfigNames::ExtraInterlanguageLinkPrefixes ) ) &&
1018 $params['prop'] !== null &&
1019 in_array( 'interwikimap', $params['prop'] )
1021 return 'anon-public-user-private';
1024 return 'public';
1027 public function getAllowedParams() {
1028 return [
1029 'prop' => [
1030 ParamValidator::PARAM_DEFAULT => 'general',
1031 ParamValidator::PARAM_ISMULTI => true,
1032 ParamValidator::PARAM_TYPE => [
1033 'general',
1034 'namespaces',
1035 'namespacealiases',
1036 'specialpagealiases',
1037 'magicwords',
1038 'interwikimap',
1039 'dbrepllag',
1040 'statistics',
1041 'usergroups',
1042 'autocreatetempuser',
1043 'libraries',
1044 'extensions',
1045 'fileextensions',
1046 'rightsinfo',
1047 'restrictions',
1048 'languages',
1049 'languagevariants',
1050 'skins',
1051 'extensiontags',
1052 'functionhooks',
1053 'showhooks',
1054 'variables',
1055 'protocols',
1056 'defaultoptions',
1057 'uploaddialog',
1059 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
1061 'filteriw' => [
1062 ParamValidator::PARAM_TYPE => [
1063 'local',
1064 '!local',
1067 'showalldb' => false,
1068 'numberingroup' => false,
1069 'inlanguagecode' => null,
1073 protected function getExamplesMessages() {
1074 return [
1075 'action=query&meta=siteinfo&siprop=general|namespaces|namespacealiases|statistics'
1076 => 'apihelp-query+siteinfo-example-simple',
1077 'action=query&meta=siteinfo&siprop=interwikimap&sifilteriw=local'
1078 => 'apihelp-query+siteinfo-example-interwiki',
1079 'action=query&meta=siteinfo&siprop=dbrepllag&sishowalldb='
1080 => 'apihelp-query+siteinfo-example-replag',
1084 public function getHelpUrls() {
1085 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Siteinfo';