Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.61% covered (success)
93.61%
601 / 642
70.00% covered (warning)
70.00%
28 / 40
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQuerySiteinfo
93.76% covered (success)
93.76%
601 / 641
70.00% covered (warning)
70.00%
28 / 40
149.04
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
1 / 1
3
 appendGeneralInfo
100.00% covered (success)
100.00%
104 / 104
100.00% covered (success)
100.00%
1 / 1
13
 appendNamespaces
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
7
 appendNamespaceAliases
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 appendSpecialPageAliases
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 appendMagicWords
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 appendInterwikiMap
97.78% covered (success)
97.78%
44 / 45
0.00% covered (danger)
0.00%
0 / 1
16
 appendDbReplLagInfo
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 appendStatistics
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 appendUserGroups
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
8
 appendAutoCreateTempUser
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 appendFileExtensions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 appendInstalledClientLibraries
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 appendInstalledLibraries
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
3.00
 appendExtensions
88.46% covered (warning)
88.46%
46 / 52
0.00% covered (danger)
0.00%
0 / 1
17.44
 appendRightsInfo
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 appendRestrictions
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 appendLanguages
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 appendLanguageVariants
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
5
 appendSkins
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 appendExtensionTags
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 appendFunctionHooks
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 appendVariables
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 appendDoubleUnderscores
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 appendProtocols
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 appendDefaultOptions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 appendUploadDialog
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAutoPromoteConds
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 processAutoPromote
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 appendAutoPromote
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 appendAutoPromoteOnce
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 appendCopyUploadDomains
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 appendSBOM
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 recAutopromote
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
9
 appendSubscribedHooks
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 getCacheMode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getAllowedParams
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\HookContainer\HookContainer;
12use MediaWiki\Interwiki\InterwikiLookup;
13use MediaWiki\Language\Language;
14use MediaWiki\Language\LanguageCode;
15use MediaWiki\Language\LanguageConverter;
16use MediaWiki\Language\LanguageConverterFactory;
17use MediaWiki\Language\LanguageFactory;
18use MediaWiki\Language\LanguageNameUtils;
19use MediaWiki\MainConfigNames;
20use MediaWiki\Parser\MagicWordFactory;
21use MediaWiki\Parser\ParserFactory;
22use MediaWiki\Permissions\GroupPermissionsLookup;
23use MediaWiki\Registration\ExtensionRegistry;
24use MediaWiki\ResourceLoader\SkinModule;
25use MediaWiki\SiteStats\SiteStats;
26use MediaWiki\Skin\Skin;
27use MediaWiki\Skin\SkinFactory;
28use MediaWiki\SpecialPage\SpecialPage;
29use MediaWiki\SpecialPage\SpecialPageFactory;
30use MediaWiki\Specials\SpecialVersion;
31use MediaWiki\Title\NamespaceInfo;
32use MediaWiki\Title\Title;
33use MediaWiki\Upload\UploadBase;
34use MediaWiki\Upload\UploadFromUrl;
35use MediaWiki\User\Options\UserOptionsLookup;
36use MediaWiki\User\TempUser\TempUserConfig;
37use MediaWiki\User\UserGroupManager;
38use MediaWiki\User\UserIdentityUtils;
39use MediaWiki\User\UserRequirementsConditionChecker;
40use MediaWiki\Utils\ExtensionInfo;
41use MediaWiki\Utils\GitInfo;
42use MediaWiki\Utils\SBOMGenerator;
43use MediaWiki\Utils\UrlUtils;
44use MediaWiki\WikiMap\WikiMap;
45use Wikimedia\ParamValidator\ParamValidator;
46use Wikimedia\Rdbms\ILoadBalancer;
47use Wikimedia\Rdbms\ReadOnlyMode;
48use Wikimedia\Timestamp\TimestampFormat as TS;
49
50/**
51 * A query action to return meta information about the wiki site.
52 *
53 * @ingroup API
54 */
55class ApiQuerySiteinfo extends ApiQueryBase {
56
57    public function __construct(
58        ApiQuery $query,
59        string $moduleName,
60        private readonly UserOptionsLookup $userOptionsLookup,
61        private readonly UserGroupManager $userGroupManager,
62        private readonly HookContainer $hookContainer,
63        private readonly LanguageConverterFactory $languageConverterFactory,
64        private readonly LanguageFactory $languageFactory,
65        private readonly LanguageNameUtils $languageNameUtils,
66        private readonly Language $contentLanguage,
67        private readonly NamespaceInfo $namespaceInfo,
68        private readonly InterwikiLookup $interwikiLookup,
69        private readonly ParserFactory $parserFactory,
70        private readonly MagicWordFactory $magicWordFactory,
71        private readonly SpecialPageFactory $specialPageFactory,
72        private readonly SkinFactory $skinFactory,
73        private readonly ILoadBalancer $loadBalancer,
74        private readonly ReadOnlyMode $readOnlyMode,
75        private readonly UrlUtils $urlUtils,
76        private readonly TempUserConfig $tempUserConfig,
77        private readonly GroupPermissionsLookup $groupPermissionsLookup,
78        private readonly SBOMGenerator $sbomGenerator,
79        private readonly UserIdentityUtils $userIdentityUtils,
80    ) {
81        parent::__construct( $query, $moduleName, 'si' );
82    }
83
84    public function execute() {
85        $params = $this->extractRequestParams();
86        $done = [];
87        foreach ( $params['prop'] as $p ) {
88            $fit = match ( $p ) {
89                'general' => $this->appendGeneralInfo( $p ),
90                'namespaces' => $this->appendNamespaces( $p ),
91                'namespacealiases' => $this->appendNamespaceAliases( $p ),
92                'specialpagealiases' => $this->appendSpecialPageAliases( $p ),
93                'magicwords' => $this->appendMagicWords( $p ),
94                'interwikimap' => $this->appendInterwikiMap( $p, $params['filteriw'] ),
95                'dbrepllag' => $this->appendDbReplLagInfo( $p, $params['showalldb'] ),
96                'statistics' => $this->appendStatistics( $p ),
97                'usergroups' => $this->appendUserGroups( $p, $params['numberingroup'] ),
98                'autocreatetempuser' => $this->appendAutoCreateTempUser( $p ),
99                'clientlibraries' => $this->appendInstalledClientLibraries( $p ),
100                'libraries' => $this->appendInstalledLibraries( $p ),
101                'extensions' => $this->appendExtensions( $p ),
102                'fileextensions' => $this->appendFileExtensions( $p ),
103                'rightsinfo' => $this->appendRightsInfo( $p ),
104                'restrictions' => $this->appendRestrictions( $p ),
105                'languages' => $this->appendLanguages( $p ),
106                'languagevariants' => $this->appendLanguageVariants( $p ),
107                'skins' => $this->appendSkins( $p ),
108                'extensiontags' => $this->appendExtensionTags( $p ),
109                'functionhooks' => $this->appendFunctionHooks( $p ),
110                'showhooks' => $this->appendSubscribedHooks( $p ),
111                'variables' => $this->appendVariables( $p ),
112                'doubleunderscores' => $this->appendDoubleUnderscores( $p ),
113                'protocols' => $this->appendProtocols( $p ),
114                'defaultoptions' => $this->appendDefaultOptions( $p ),
115                'uploaddialog' => $this->appendUploadDialog( $p ),
116                'autopromote' => $this->appendAutoPromote( $p ),
117                'autopromoteonce' => $this->appendAutoPromoteOnce( $p ),
118                'copyuploaddomains' => $this->appendCopyUploadDomains( $p ),
119                'sbom' => $this->appendSBOM( $p ),
120                // @phan-suppress-next-line PhanUseReturnValueOfNever
121                default => ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" ) // @codeCoverageIgnore
122            };
123            if ( !$fit ) {
124                // Abuse siprop as a query-continue parameter
125                // and set it to all unprocessed props
126                $this->setContinueEnumParameter( 'prop', implode( '|',
127                    array_diff( $params['prop'], $done ) ) );
128                break;
129            }
130            $done[] = $p;
131        }
132    }
133
134    protected function appendGeneralInfo( string $property ): bool {
135        $config = $this->getConfig();
136        $mainPage = Title::newMainPage();
137        $logo = SkinModule::getAvailableLogos( $config, $this->getLanguage()->getCode() );
138
139        $data = [
140            'mainpage' => $mainPage->getPrefixedText(),
141            'base' => (string)$this->urlUtils->expand( $mainPage->getFullURL(), PROTO_CURRENT ),
142            'sitename' => $config->get( MainConfigNames::Sitename ),
143            'mainpageisdomainroot' => (bool)$config->get( MainConfigNames::MainPageIsDomainRoot ),
144
145            // A logo can either be a relative or an absolute path
146            // make sure we always return an absolute path
147            'logo' => (string)$this->urlUtils->expand( $logo['1x'], PROTO_RELATIVE ),
148
149            'generator' => 'MediaWiki ' . MW_VERSION,
150
151            'phpversion' => PHP_VERSION,
152            'phpsapi' => PHP_SAPI,
153            'dbtype' => $config->get( MainConfigNames::DBtype ),
154            'dbversion' => $this->getDB()->getServerVersion(),
155        ];
156
157        $allowFrom = [ '' ];
158        $allowException = true;
159        if ( !$config->get( MainConfigNames::AllowExternalImages ) ) {
160            $data['imagewhitelistenabled'] =
161                (bool)$config->get( MainConfigNames::EnableImageWhitelist );
162            $allowFrom = $config->get( MainConfigNames::AllowExternalImagesFrom );
163            $allowException = (bool)$allowFrom;
164        }
165        if ( $allowException ) {
166            $data['externalimages'] = (array)$allowFrom;
167            ApiResult::setIndexedTagName( $data['externalimages'], 'prefix' );
168        }
169
170        $data['langconversion'] = !$this->languageConverterFactory->isConversionDisabled();
171        $data['linkconversion'] = !$this->languageConverterFactory->isLinkConversionDisabled();
172        // For backwards compatibility (soft deprecated since MW 1.36)
173        $data['titleconversion'] = $data['linkconversion'];
174
175        $contLangConverter = $this->languageConverterFactory->getLanguageConverter( $this->contentLanguage );
176        if ( $this->contentLanguage->linkPrefixExtension() ) {
177            $linkPrefixCharset = $this->contentLanguage->linkPrefixCharset();
178            $data['linkprefixcharset'] = $linkPrefixCharset;
179            // For backwards compatibility
180            $data['linkprefix'] = "/^((?>.*[^$linkPrefixCharset]|))(.+)$/sDu";
181        } else {
182            $data['linkprefixcharset'] = '';
183            $data['linkprefix'] = '';
184        }
185
186        $data['linktrail'] = $this->contentLanguage->linkTrail() ?: '';
187
188        $data['legaltitlechars'] = Title::legalChars();
189        $data['invalidusernamechars'] = $config->get( MainConfigNames::InvalidUsernameCharacters );
190        $data['allunicodefixes'] = (bool)$config->get( MainConfigNames::AllUnicodeFixes );
191
192        $git = GitInfo::repo()->getHeadSHA1();
193        if ( $git ) {
194            $data['git-hash'] = $git;
195            $data['git-branch'] = GitInfo::repo()->getCurrentBranch();
196        }
197
198        // 'case-insensitive' option is reserved for future
199        $data['case'] =
200            $config->get( MainConfigNames::CapitalLinks ) ? 'first-letter' : 'case-sensitive';
201        $data['lang'] = $config->get( MainConfigNames::LanguageCode );
202
203        $data['fallback'] = [];
204        foreach ( $this->contentLanguage->getFallbackLanguages() as $code ) {
205            $data['fallback'][] = [ 'code' => $code ];
206        }
207        ApiResult::setIndexedTagName( $data['fallback'], 'lang' );
208
209        if ( $contLangConverter->hasVariants() ) {
210            $data['variants'] = [];
211            foreach ( $contLangConverter->getVariants() as $code ) {
212                $data['variants'][] = [
213                    'code' => $code,
214                    'name' => $this->contentLanguage->getVariantname( $code ),
215                ];
216            }
217            ApiResult::setIndexedTagName( $data['variants'], 'lang' );
218        }
219
220        $data['rtl'] = $this->contentLanguage->isRTL();
221        $data['fallback8bitEncoding'] = $this->contentLanguage->fallback8bitEncoding();
222
223        $data['readonly'] = $this->readOnlyMode->isReadOnly();
224        if ( $data['readonly'] ) {
225            $data['readonlyreason'] = $this->readOnlyMode->getReason();
226        }
227        $data['writeapi'] = true; // Deprecated since MW 1.32
228
229        $data['maxarticlesize'] = $config->get( MainConfigNames::MaxArticleSize ) * 1024;
230
231        $data['timezone'] = $config->get( MainConfigNames::Localtimezone );
232        $data['timeoffset'] = (int)( $config->get( MainConfigNames::LocalTZoffset ) );
233        $data['articlepath'] = $config->get( MainConfigNames::ArticlePath );
234        $data['scriptpath'] = $config->get( MainConfigNames::ScriptPath );
235        $data['script'] = $config->get( MainConfigNames::Script );
236        $data['variantarticlepath'] = $config->get( MainConfigNames::VariantArticlePath );
237        $data[ApiResult::META_BC_BOOLS][] = 'variantarticlepath';
238        $data['server'] = $config->get( MainConfigNames::Server );
239        $data['servername'] = $config->get( MainConfigNames::ServerName );
240        $data['wikiid'] = WikiMap::getCurrentWikiId();
241        $data['time'] = wfTimestamp( TS::ISO_8601, time() );
242
243        $data['misermode'] = (bool)$config->get( MainConfigNames::MiserMode );
244
245        $data['uploadsenabled'] = UploadBase::isEnabled();
246        $data['maxuploadsize'] = UploadBase::getMaxUploadSize();
247        $data['minuploadchunksize'] = ApiUpload::getMinUploadChunkSize( $config );
248
249        $data['galleryoptions'] = $config->get( MainConfigNames::GalleryOptions );
250
251        $data['thumblimits'] = $config->get( MainConfigNames::ThumbLimits );
252        ApiResult::setArrayType( $data['thumblimits'], 'BCassoc' );
253        ApiResult::setIndexedTagName( $data['thumblimits'], 'limit' );
254        $data['imagelimits'] = [];
255        foreach ( $config->get( MainConfigNames::ImageLimits ) as $k => $limit ) {
256            $data['imagelimits'][$k] = [ 'width' => $limit[0], 'height' => $limit[1] ];
257        }
258        ApiResult::setArrayType( $data['imagelimits'], 'BCassoc' );
259        ApiResult::setIndexedTagName( $data['imagelimits'], 'limit' );
260
261        $favicon = $config->get( MainConfigNames::Favicon );
262        if ( $favicon ) {
263            // Expand any local path to full URL to improve API usability (T77093).
264            $data['favicon'] = (string)$this->urlUtils->expand( $favicon );
265        }
266
267        $data['centralidlookupprovider'] = $config->get( MainConfigNames::CentralIdLookupProvider );
268        $providerIds = array_keys( $config->get( MainConfigNames::CentralIdLookupProviders ) );
269        $data['allcentralidlookupproviders'] = $providerIds;
270
271        $data['interwikimagic'] = (bool)$config->get( MainConfigNames::InterwikiMagic );
272        $data['magiclinks'] = $config->get( MainConfigNames::EnableMagicLinks );
273
274        $data['categorycollation'] = $config->get( MainConfigNames::CategoryCollation );
275
276        $data['nofollowlinks'] = $config->get( MainConfigNames::NoFollowLinks );
277        $data['nofollownsexceptions'] = $config->get( MainConfigNames::NoFollowNsExceptions );
278        $data['nofollowdomainexceptions'] = $config->get( MainConfigNames::NoFollowDomainExceptions );
279        $data['externallinktarget'] = $config->get( MainConfigNames::ExternalLinkTarget );
280
281        $this->getHookRunner()->onAPIQuerySiteInfoGeneralInfo( $this, $data );
282
283        return $this->getResult()->addValue( 'query', $property, $data );
284    }
285
286    protected function appendNamespaces( string $property ): bool {
287        $nsProtection = $this->getConfig()->get( MainConfigNames::NamespaceProtection );
288
289        $data = [ ApiResult::META_TYPE => 'assoc' ];
290        foreach ( $this->contentLanguage->getFormattedNamespaces() as $ns => $title ) {
291            $data[$ns] = [
292                'id' => (int)$ns,
293                'case' => $this->namespaceInfo->isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
294            ];
295            ApiResult::setContentValue( $data[$ns], 'name', $title );
296            $canonical = $this->namespaceInfo->getCanonicalName( $ns );
297
298            $data[$ns]['subpages'] = $this->namespaceInfo->hasSubpages( $ns );
299
300            if ( $canonical ) {
301                $data[$ns]['canonical'] = strtr( $canonical, '_', ' ' );
302            }
303
304            $data[$ns]['content'] = $this->namespaceInfo->isContent( $ns );
305            $data[$ns]['nonincludable'] = $this->namespaceInfo->isNonincludable( $ns );
306
307            $specificNs = $nsProtection[$ns] ?? '';
308            if ( is_array( $specificNs ) ) {
309                $specificNs = implode( "|", array_filter( $specificNs ) );
310            }
311            if ( $specificNs !== '' ) {
312                $data[$ns]['namespaceprotection'] = $specificNs;
313            }
314
315            $contentmodel = $this->namespaceInfo->getNamespaceContentModel( $ns );
316            if ( $contentmodel ) {
317                $data[$ns]['defaultcontentmodel'] = $contentmodel;
318            }
319        }
320
321        ApiResult::setArrayType( $data, 'assoc' );
322        ApiResult::setIndexedTagName( $data, 'ns' );
323
324        return $this->getResult()->addValue( 'query', $property, $data );
325    }
326
327    protected function appendNamespaceAliases( string $property ): bool {
328        $aliases = $this->contentLanguage->getNamespaceAliases();
329        $namespaces = $this->contentLanguage->getNamespaces();
330        $data = [];
331        foreach ( $aliases as $title => $ns ) {
332            if ( $namespaces[$ns] == $title ) {
333                // Don't list duplicates
334                continue;
335            }
336            $item = [ 'id' => (int)$ns ];
337            ApiResult::setContentValue( $item, 'alias', strtr( $title, '_', ' ' ) );
338            $data[] = $item;
339        }
340
341        sort( $data );
342
343        ApiResult::setIndexedTagName( $data, 'ns' );
344
345        return $this->getResult()->addValue( 'query', $property, $data );
346    }
347
348    protected function appendSpecialPageAliases( string $property ): bool {
349        $data = [];
350        $aliases = $this->contentLanguage->getSpecialPageAliases();
351        foreach ( $this->specialPageFactory->getNames() as $specialpage ) {
352            if ( isset( $aliases[$specialpage] ) ) {
353                $arr = [ 'realname' => $specialpage, 'aliases' => $aliases[$specialpage] ];
354                ApiResult::setIndexedTagName( $arr['aliases'], 'alias' );
355                $data[] = $arr;
356            }
357        }
358        ApiResult::setIndexedTagName( $data, 'specialpage' );
359
360        return $this->getResult()->addValue( 'query', $property, $data );
361    }
362
363    protected function appendMagicWords( string $property ): bool {
364        $data = [];
365        foreach ( $this->contentLanguage->getMagicWords() as $name => $aliases ) {
366            $caseSensitive = (bool)array_shift( $aliases );
367            $arr = [
368                'name' => $name,
369                'aliases' => $aliases,
370                'case-sensitive' => $caseSensitive,
371            ];
372            ApiResult::setIndexedTagName( $arr['aliases'], 'alias' );
373            $data[] = $arr;
374        }
375        ApiResult::setIndexedTagName( $data, 'magicword' );
376
377        return $this->getResult()->addValue( 'query', $property, $data );
378    }
379
380    protected function appendInterwikiMap( string $property, ?string $filter ): bool {
381        $local = $filter ? $filter === 'local' : null;
382
383        $params = $this->extractRequestParams();
384        $langCode = $params['inlanguagecode'] ?? '';
385        $interwikiMagic = $this->getConfig()->get( MainConfigNames::InterwikiMagic );
386
387        if ( $interwikiMagic ) {
388            $langNames = $this->languageNameUtils->getLanguageNames( $langCode );
389        }
390
391        $extraLangPrefixes = $this->getConfig()->get( MainConfigNames::ExtraInterlanguageLinkPrefixes );
392        $extraLangCodeMap = $this->getConfig()->get( MainConfigNames::InterlanguageLinkCodeMap );
393        $localInterwikis = $this->getConfig()->get( MainConfigNames::LocalInterwikis );
394        $data = [];
395
396        foreach ( $this->interwikiLookup->getAllPrefixes( $local ) as $row ) {
397            $prefix = $row['iw_prefix'];
398            $val = [];
399            $val['prefix'] = $prefix;
400            if ( $row['iw_local'] ?? false ) {
401                $val['local'] = true;
402            }
403            if ( $row['iw_trans'] ?? false ) {
404                $val['trans'] = true;
405            }
406
407            if ( $interwikiMagic && isset( $langNames[$prefix] ) ) {
408                $val['language'] = $langNames[$prefix];
409                $standard = LanguageCode::replaceDeprecatedCodes( $prefix );
410                if ( $standard !== $prefix ) {
411                    # Note that even if this code is deprecated, it should
412                    # only be remapped if extralanglink (set below) is false.
413                    $val['deprecated'] = $standard;
414                }
415                $val['bcp47'] = LanguageCode::bcp47( $standard );
416            }
417            if ( in_array( $prefix, $localInterwikis ) ) {
418                $val['localinterwiki'] = true;
419            }
420            if ( $interwikiMagic && in_array( $prefix, $extraLangPrefixes ) ) {
421                $val['extralanglink'] = true;
422                $val['code'] = $extraLangCodeMap[$prefix] ?? $prefix;
423                $val['bcp47'] = LanguageCode::bcp47( $val['code'] );
424
425                $linktext = $this->msg( "interlanguage-link-$prefix" );
426                if ( !$linktext->isDisabled() ) {
427                    $val['linktext'] = $linktext->text();
428                }
429
430                $sitename = $this->msg( "interlanguage-link-sitename-$prefix" );
431                if ( !$sitename->isDisabled() ) {
432                    $val['sitename'] = $sitename->text();
433                }
434            }
435
436            $val['url'] = (string)$this->urlUtils->expand( $row['iw_url'], PROTO_CURRENT );
437            $val['protorel'] = str_starts_with( $row['iw_url'], '//' );
438            if ( ( $row['iw_wikiid'] ?? '' ) !== '' ) {
439                $val['wikiid'] = $row['iw_wikiid'];
440            }
441            if ( ( $row['iw_api'] ?? '' ) !== '' ) {
442                $val['api'] = $row['iw_api'];
443            }
444
445            $data[] = $val;
446        }
447
448        ApiResult::setIndexedTagName( $data, 'iw' );
449
450        return $this->getResult()->addValue( 'query', $property, $data );
451    }
452
453    protected function appendDbReplLagInfo( string $property, bool $includeAll ): bool {
454        $data = [];
455        $showHostnames = $this->getConfig()->get( MainConfigNames::ShowHostnames );
456        if ( $includeAll ) {
457            if ( !$showHostnames ) {
458                $this->dieWithError( 'apierror-siteinfo-includealldenied', 'includeAllDenied' );
459            }
460
461            foreach ( $this->loadBalancer->getLagTimes() as $i => $lag ) {
462                $data[] = [
463                    'host' => $this->loadBalancer->getServerName( $i ),
464                    'lag' => $lag
465                ];
466            }
467        } else {
468            [ , $lag, $index ] = $this->loadBalancer->getMaxLag();
469            $data[] = [
470                'host' => $showHostnames ? $this->loadBalancer->getServerName( $index ) : '',
471                'lag' => $lag
472            ];
473        }
474
475        ApiResult::setIndexedTagName( $data, 'db' );
476
477        return $this->getResult()->addValue( 'query', $property, $data );
478    }
479
480    protected function appendStatistics( string $property ): bool {
481        $data = [
482            'pages' => SiteStats::pages(),
483            'articles' => SiteStats::articles(),
484            'edits' => SiteStats::edits(),
485            'images' => SiteStats::images(),
486            'users' => SiteStats::users(),
487            'activeusers' => SiteStats::activeUsers(),
488            'admins' => SiteStats::numberingroup( 'sysop' ),
489            'jobs' => SiteStats::jobs(),
490        ];
491
492        $this->getHookRunner()->onAPIQuerySiteInfoStatisticsInfo( $data );
493
494        return $this->getResult()->addValue( 'query', $property, $data );
495    }
496
497    protected function appendUserGroups( string $property, bool $numberInGroup ): bool {
498        $config = $this->getConfig();
499
500        $data = [];
501        $result = $this->getResult();
502        $allGroups = $this->userGroupManager->listAllGroups();
503        $allImplicitGroups = $this->userGroupManager->listAllImplicitGroups();
504        foreach ( array_merge( $allImplicitGroups, $allGroups ) as $group ) {
505            $arr = [
506                'name' => $group,
507                'rights' => $this->groupPermissionsLookup->getGrantedPermissions( $group ),
508                // TODO: Also expose the list of revoked permissions somehow.
509            ];
510
511            if ( $numberInGroup ) {
512                $autopromote = $config->get( MainConfigNames::Autopromote );
513
514                if ( $group == 'user' ) {
515                    $arr['number'] = SiteStats::users();
516                // '*' and autopromote groups have no size
517                } elseif ( $group !== '*' && !isset( $autopromote[$group] ) ) {
518                    $arr['number'] = SiteStats::numberingroup( $group );
519                }
520            }
521
522            $groupArr = $this->userGroupManager->getGroupsChangeableByGroup( $group );
523
524            foreach ( $groupArr as $type => $groups ) {
525                $groups = array_values( array_intersect( $groups, $allGroups ) );
526                if ( $groups ) {
527                    $arr[$type] = $groups;
528                    ApiResult::setArrayType( $arr[$type], 'BCarray' );
529                    ApiResult::setIndexedTagName( $arr[$type], 'group' );
530                }
531            }
532
533            ApiResult::setIndexedTagName( $arr['rights'], 'permission' );
534            $data[] = $arr;
535        }
536
537        ApiResult::setIndexedTagName( $data, 'group' );
538
539        return $result->addValue( 'query', $property, $data );
540    }
541
542    protected function appendAutoCreateTempUser( string $property ): bool {
543        $data = [ 'enabled' => $this->tempUserConfig->isEnabled() ];
544        if ( $this->tempUserConfig->isKnown() ) {
545            $data['matchPatterns'] = $this->tempUserConfig->getMatchPatterns();
546        }
547        return $this->getResult()->addValue( 'query', $property, $data );
548    }
549
550    protected function appendFileExtensions( string $property ): bool {
551        $data = [];
552        foreach (
553            array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) ) as $ext
554        ) {
555            $data[] = [ 'ext' => $ext ];
556        }
557        ApiResult::setIndexedTagName( $data, 'fe' );
558
559        return $this->getResult()->addValue( 'query', $property, $data );
560    }
561
562    protected function appendInstalledClientLibraries( string $property ): bool {
563        $data = [];
564        foreach ( SpecialVersion::parseForeignResources() as $name => $info ) {
565            $data[] = [
566                // Can't use $name as it is version suffixed (as multiple versions
567                // of a library may exist, provided by different skins/extensions)
568                'name' => $info['name'],
569                'version' => $info['version'],
570            ];
571        }
572        ApiResult::setIndexedTagName( $data, 'library' );
573        return $this->getResult()->addValue( 'query', $property, $data );
574    }
575
576    protected function appendInstalledLibraries( string $property ): bool {
577        $credits = SpecialVersion::getCredits(
578            ExtensionRegistry::getInstance(),
579            $this->getConfig()
580        );
581        $data = [];
582        foreach ( SpecialVersion::parseComposerInstalled( $credits ) as $name => $info ) {
583            if ( str_starts_with( $info['type'], 'mediawiki-' ) ) {
584                // Skip any extensions or skins since they'll be listed
585                // in their proper section
586                continue;
587            }
588            $data[] = [
589                'name' => $name,
590                'version' => $info['version'],
591            ];
592        }
593        ApiResult::setIndexedTagName( $data, 'library' );
594
595        return $this->getResult()->addValue( 'query', $property, $data );
596    }
597
598    protected function appendExtensions( string $property ): bool {
599        $data = [];
600        $credits = SpecialVersion::getCredits(
601            ExtensionRegistry::getInstance(),
602            $this->getConfig()
603        );
604        foreach ( $credits as $type => $extensions ) {
605            foreach ( $extensions as $ext ) {
606                $ret = [ 'type' => $type ];
607                if ( isset( $ext['name'] ) ) {
608                    $ret['name'] = $ext['name'];
609                }
610                if ( isset( $ext['namemsg'] ) ) {
611                    $ret['namemsg'] = $ext['namemsg'];
612                }
613                if ( isset( $ext['description'] ) ) {
614                    $ret['description'] = $ext['description'];
615                }
616                if ( isset( $ext['descriptionmsg'] ) ) {
617                    // Can be a string or [ key, param1, param2, ... ]
618                    if ( is_array( $ext['descriptionmsg'] ) ) {
619                        $ret['descriptionmsg'] = $ext['descriptionmsg'][0];
620                        $ret['descriptionmsgparams'] = array_slice( $ext['descriptionmsg'], 1 );
621                        ApiResult::setIndexedTagName( $ret['descriptionmsgparams'], 'param' );
622                    } else {
623                        $ret['descriptionmsg'] = $ext['descriptionmsg'];
624                    }
625                }
626                if ( isset( $ext['author'] ) ) {
627                    $ret['author'] = is_array( $ext['author'] ) ?
628                        implode( ', ', $ext['author'] ) : $ext['author'];
629                }
630                if ( isset( $ext['url'] ) ) {
631                    $ret['url'] = $ext['url'];
632                }
633                if ( isset( $ext['version'] ) ) {
634                    $ret['version'] = $ext['version'];
635                }
636                if ( isset( $ext['path'] ) ) {
637                    $extensionPath = dirname( $ext['path'] );
638                    $gitInfo = new GitInfo( $extensionPath );
639                    $vcsVersion = $gitInfo->getHeadSHA1();
640                    if ( $vcsVersion !== false ) {
641                        $ret['vcs-system'] = 'git';
642                        $ret['vcs-version'] = $vcsVersion;
643                        $ret['vcs-url'] = $gitInfo->getHeadViewUrl();
644                        $vcsDate = $gitInfo->getHeadCommitDate();
645                        if ( $vcsDate !== false ) {
646                            $ret['vcs-date'] = wfTimestamp( TS::ISO_8601, $vcsDate );
647                        }
648                    }
649
650                    if ( ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
651                        $ret['license-name'] = $ext['license-name'] ?? '';
652                        $ret['license'] = SpecialPage::getTitleFor(
653                            'Version',
654                            "License/{$ext['name']}"
655                        )->getLinkURL();
656                    }
657
658                    if ( ExtensionInfo::getAuthorsFileName( $extensionPath ) ) {
659                        $ret['credits'] = SpecialPage::getTitleFor(
660                            'Version',
661                            "Credits/{$ext['name']}"
662                        )->getLinkURL();
663                    }
664                }
665                $data[] = $ret;
666            }
667        }
668
669        ApiResult::setIndexedTagName( $data, 'ext' );
670
671        return $this->getResult()->addValue( 'query', $property, $data );
672    }
673
674    protected function appendRightsInfo( string $property ): bool {
675        $config = $this->getConfig();
676        $title = Title::newFromText( $config->get( MainConfigNames::RightsPage ) );
677        if ( $title ) {
678            $url = $this->urlUtils->expand( $title->getLinkURL(), PROTO_CURRENT );
679        } else {
680            $url = $config->get( MainConfigNames::RightsUrl );
681        }
682        $text = $config->get( MainConfigNames::RightsText ) ?? '';
683        if ( $text === '' && $title ) {
684            $text = $title->getPrefixedText();
685        }
686
687        $data = [
688            'url' => (string)$url,
689            'text' => (string)$text,
690        ];
691
692        return $this->getResult()->addValue( 'query', $property, $data );
693    }
694
695    protected function appendRestrictions( string $property ): bool {
696        $config = $this->getConfig();
697        $data = [
698            'types' => $config->get( MainConfigNames::RestrictionTypes ),
699            'levels' => $config->get( MainConfigNames::RestrictionLevels ),
700            'cascadinglevels' => $config->get( MainConfigNames::CascadingRestrictionLevels ),
701            'semiprotectedlevels' => $config->get( MainConfigNames::SemiprotectedRestrictionLevels ),
702        ];
703
704        ApiResult::setArrayType( $data['types'], 'BCarray' );
705        ApiResult::setArrayType( $data['levels'], 'BCarray' );
706        ApiResult::setArrayType( $data['cascadinglevels'], 'BCarray' );
707        ApiResult::setArrayType( $data['semiprotectedlevels'], 'BCarray' );
708
709        ApiResult::setIndexedTagName( $data['types'], 'type' );
710        ApiResult::setIndexedTagName( $data['levels'], 'level' );
711        ApiResult::setIndexedTagName( $data['cascadinglevels'], 'level' );
712        ApiResult::setIndexedTagName( $data['semiprotectedlevels'], 'level' );
713
714        return $this->getResult()->addValue( 'query', $property, $data );
715    }
716
717    public function appendLanguages( string $property ): bool {
718        $params = $this->extractRequestParams();
719        $langCode = $params['inlanguagecode'] ?? '';
720        $langNames = $this->languageNameUtils->getLanguageNames( $langCode );
721
722        $data = [];
723
724        foreach ( $langNames as $code => $name ) {
725            $lang = [
726                'code' => $code,
727                'bcp47' => LanguageCode::bcp47( $code ),
728            ];
729            ApiResult::setContentValue( $lang, 'name', $name );
730            $data[] = $lang;
731        }
732        ApiResult::setIndexedTagName( $data, 'lang' );
733
734        return $this->getResult()->addValue( 'query', $property, $data );
735    }
736
737    // Export information about which page languages will trigger
738    // language conversion. (T153341)
739    public function appendLanguageVariants( string $property ): bool {
740        $langNames = $this->languageConverterFactory->isConversionDisabled() ? [] :
741            LanguageConverter::$languagesWithVariants;
742        sort( $langNames );
743
744        $data = [];
745        foreach ( $langNames as $langCode ) {
746            $lang = $this->languageFactory->getLanguage( $langCode );
747            $langConverter = $this->languageConverterFactory->getLanguageConverter( $lang );
748            if ( !$langConverter->hasVariants() ) {
749                // Only languages which have variants should be listed
750                continue;
751            }
752            $data[$langCode] = [];
753            ApiResult::setIndexedTagName( $data[$langCode], 'variant' );
754            ApiResult::setArrayType( $data[$langCode], 'kvp', 'code' );
755
756            $variants = $langConverter->getVariants();
757            sort( $variants );
758            foreach ( $variants as $v ) {
759                $data[$langCode][$v] = [
760                    'fallbacks' => (array)$langConverter->getVariantFallbacks( $v ),
761                ];
762                ApiResult::setIndexedTagName(
763                    $data[$langCode][$v]['fallbacks'], 'variant'
764                );
765            }
766        }
767        ApiResult::setIndexedTagName( $data, 'lang' );
768        ApiResult::setArrayType( $data, 'kvp', 'code' );
769
770        return $this->getResult()->addValue( 'query', $property, $data );
771    }
772
773    public function appendSkins( string $property ): bool {
774        $data = [];
775        $allowed = $this->skinFactory->getAllowedSkins();
776        $default = Skin::normalizeKey( 'default' );
777
778        foreach ( $this->skinFactory->getInstalledSkins() as $name => $displayName ) {
779            $msg = $this->msg( "skinname-{$name}" );
780            $code = $this->getParameter( 'inlanguagecode' );
781            if ( $code && $this->languageNameUtils->isValidCode( $code ) ) {
782                $msg->inLanguage( $code );
783            } else {
784                $msg->inContentLanguage();
785            }
786            if ( $msg->exists() ) {
787                $displayName = $msg->text();
788            }
789            $skin = [ 'code' => $name ];
790            ApiResult::setContentValue( $skin, 'name', $displayName );
791            if ( !isset( $allowed[$name] ) ) {
792                $skin['unusable'] = true;
793            }
794            if ( $name === $default ) {
795                $skin['default'] = true;
796            }
797            $data[] = $skin;
798        }
799        ApiResult::setIndexedTagName( $data, 'skin' );
800
801        return $this->getResult()->addValue( 'query', $property, $data );
802    }
803
804    public function appendExtensionTags( string $property ): bool {
805        $tags = array_map(
806            static fn ( $item ) => "<$item>",
807            $this->parserFactory->getMainInstance()->getTags()
808        );
809        ApiResult::setArrayType( $tags, 'BCarray' );
810        ApiResult::setIndexedTagName( $tags, 't' );
811
812        return $this->getResult()->addValue( 'query', $property, $tags );
813    }
814
815    public function appendFunctionHooks( string $property ): bool {
816        $hooks = $this->parserFactory->getMainInstance()->getFunctionHooks();
817        ApiResult::setArrayType( $hooks, 'BCarray' );
818        ApiResult::setIndexedTagName( $hooks, 'h' );
819
820        return $this->getResult()->addValue( 'query', $property, $hooks );
821    }
822
823    public function appendVariables( string $property ): bool {
824        $variables = $this->magicWordFactory->getVariableIDs();
825        ApiResult::setArrayType( $variables, 'BCarray' );
826        ApiResult::setIndexedTagName( $variables, 'v' );
827
828        return $this->getResult()->addValue( 'query', $property, $variables );
829    }
830
831    public function appendDoubleUnderscores( string $property ): bool {
832        $ids = $this->magicWordFactory->getDoubleUnderscoreArray()->getNames();
833        ApiResult::setArrayType( $ids, 'BCarray' );
834        ApiResult::setIndexedTagName( $ids, 'v' );
835
836        return $this->getResult()->addValue( 'query', $property, $ids );
837    }
838
839    public function appendProtocols( string $property ): bool {
840        // Make a copy of the global so we don't try to set the _element key of it - T47130
841        $protocols = array_values( $this->getConfig()->get( MainConfigNames::UrlProtocols ) );
842        ApiResult::setArrayType( $protocols, 'BCarray' );
843        ApiResult::setIndexedTagName( $protocols, 'p' );
844
845        return $this->getResult()->addValue( 'query', $property, $protocols );
846    }
847
848    public function appendDefaultOptions( string $property ): bool {
849        $options = $this->userOptionsLookup->getDefaultOptions( null );
850        $options[ApiResult::META_BC_BOOLS] = array_keys( $options );
851        return $this->getResult()->addValue( 'query', $property, $options );
852    }
853
854    public function appendUploadDialog( string $property ): bool {
855        $config = $this->getConfig()->get( MainConfigNames::UploadDialog );
856        return $this->getResult()->addValue( 'query', $property, $config );
857    }
858
859    private function getAutoPromoteConds(): array {
860        $allowedConditions = [];
861        foreach ( get_defined_constants() as $constantName => $constantValue ) {
862            if ( str_contains( $constantName, 'APCOND_' ) ) {
863                $allowedConditions[$constantName] = $constantValue;
864            }
865        }
866        return $allowedConditions;
867    }
868
869    private function processAutoPromote( array $input, array $allowedConditions ): array {
870        $data = [];
871        foreach ( $input as $groupName => $conditions ) {
872            $row = $this->recAutopromote( $conditions, $allowedConditions );
873            if ( !isset( $row[0] ) || is_string( $row ) ) {
874                $row = [ $row ];
875            }
876            $data[$groupName] = $row;
877        }
878        return $data;
879    }
880
881    private function appendAutoPromote( string $property ): bool {
882        return $this->getResult()->addValue(
883            'query',
884            $property,
885            $this->processAutoPromote(
886                $this->getConfig()->get( MainConfigNames::Autopromote ),
887                $this->getAutoPromoteConds()
888            )
889        );
890    }
891
892    private function appendAutoPromoteOnce( string $property ): bool {
893        $allowedConditions = $this->getAutoPromoteConds();
894        $data = [];
895        foreach ( $this->getConfig()->get( MainConfigNames::AutopromoteOnce ) as $key => $value ) {
896            $data[$key] = $this->processAutoPromote( $value, $allowedConditions );
897        }
898        return $this->getResult()->addValue( 'query', $property, $data );
899    }
900
901    private function appendCopyUploadDomains( string $property ): bool {
902        $allowedHosts = UploadFromUrl::getAllowedHosts();
903        ApiResult::setIndexedTagName( $allowedHosts, 'domain' );
904        return $this->getResult()->addValue(
905            'query',
906            $property,
907            $allowedHosts
908        );
909    }
910
911    protected function appendSBOM( string $property ): bool {
912        if ( !$this->userIdentityUtils->isNamed( $this->getUser() ) ) {
913            $this->dieWithError( 'apierror-mustbeloggedin-sbom' );
914        }
915        $data = $this->sbomGenerator->generateCdxSBOM( $this->getContext() );
916        return $this->getResult()->addValue( 'query', $property, $data );
917    }
918
919    /**
920     * @param array|int|string $cond
921     * @param array $allowedConditions
922     * @return array|string
923     */
924    private function recAutopromote( $cond, $allowedConditions ) {
925        $config = [];
926        // First, checks if $cond is an array
927        if ( is_array( $cond ) ) {
928            // Checks if $cond[0] is a valid operand
929            if ( in_array( $cond[0], UserRequirementsConditionChecker::VALID_OPS, true ) ) {
930                $config['operand'] = $cond[0];
931                // Traversal checks conditions
932                foreach ( array_slice( $cond, 1 ) as $value ) {
933                    $config[] = $this->recAutopromote( $value, $allowedConditions );
934                }
935            } elseif ( is_string( $cond[0] ) ) {
936                // Returns $cond directly, if $cond[0] is a string
937                $config = $cond;
938            } else {
939                // When $cond is equal to an APCOND_ constant value
940                $params = array_slice( $cond, 1 );
941                if ( $params === [ null ] ) {
942                    // Special casing for these conditions and their default of null,
943                    // to replace their values with $wgAutoConfirmCount/$wgAutoConfirmAge as appropriate
944                    if ( $cond[0] === APCOND_EDITCOUNT ) {
945                        $params = [ $this->getConfig()->get( MainConfigNames::AutoConfirmCount ) ];
946                    } elseif ( $cond[0] === APCOND_AGE ) {
947                        $params = [ $this->getConfig()->get( MainConfigNames::AutoConfirmAge ) ];
948                    }
949                }
950                $config = [
951                    'condname' => array_search( $cond[0], $allowedConditions ),
952                    'params' => $params
953                ];
954                ApiResult::setIndexedTagName( $config, 'params' );
955            }
956        } elseif ( is_string( $cond ) ) {
957            $config = $cond;
958        } else {
959            // When $cond is equal to an APCOND_ constant value
960            $config = [
961                'condname' => array_search( $cond, $allowedConditions ),
962                'params' => []
963            ];
964            ApiResult::setIndexedTagName( $config, 'params' );
965        }
966
967        return $config;
968    }
969
970    public function appendSubscribedHooks( string $property ): bool {
971        $hookNames = $this->hookContainer->getHookNames();
972        sort( $hookNames );
973
974        $data = [];
975        foreach ( $hookNames as $name ) {
976            $arr = [
977                'name' => $name,
978                'subscribers' => $this->hookContainer->getHandlerDescriptions( $name ),
979            ];
980
981            ApiResult::setArrayType( $arr['subscribers'], 'array' );
982            ApiResult::setIndexedTagName( $arr['subscribers'], 's' );
983            $data[] = $arr;
984        }
985
986        ApiResult::setIndexedTagName( $data, 'hook' );
987
988        return $this->getResult()->addValue( 'query', $property, $data );
989    }
990
991    /** @inheritDoc */
992    public function getCacheMode( $params ) {
993        // Messages for $wgExtraInterlanguageLinkPrefixes depend on user language
994        if ( $this->getConfig()->get( MainConfigNames::ExtraInterlanguageLinkPrefixes ) &&
995            in_array( 'interwikimap', $params['prop'] ?? [] )
996        ) {
997            return 'anon-public-user-private';
998        }
999
1000        return 'public';
1001    }
1002
1003    /** @inheritDoc */
1004    public function getAllowedParams() {
1005        return [
1006            'prop' => [
1007                ParamValidator::PARAM_DEFAULT => 'general',
1008                ParamValidator::PARAM_ISMULTI => true,
1009                ParamValidator::PARAM_TYPE => [
1010                    'general',
1011                    'namespaces',
1012                    'namespacealiases',
1013                    'specialpagealiases',
1014                    'magicwords',
1015                    'interwikimap',
1016                    'dbrepllag',
1017                    'statistics',
1018                    'usergroups',
1019                    'autocreatetempuser',
1020                    'clientlibraries',
1021                    'libraries',
1022                    'extensions',
1023                    'fileextensions',
1024                    'rightsinfo',
1025                    'restrictions',
1026                    'languages',
1027                    'languagevariants',
1028                    'skins',
1029                    'extensiontags',
1030                    'functionhooks',
1031                    'showhooks',
1032                    'variables',
1033                    'doubleunderscores',
1034                    'protocols',
1035                    'defaultoptions',
1036                    'uploaddialog',
1037                    'autopromote',
1038                    'autopromoteonce',
1039                    'copyuploaddomains',
1040                    'sbom',
1041                ],
1042                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
1043            ],
1044            'filteriw' => [
1045                ParamValidator::PARAM_TYPE => [
1046                    'local',
1047                    '!local',
1048                ]
1049            ],
1050            'showalldb' => false,
1051            'numberingroup' => false,
1052            'inlanguagecode' => null,
1053        ];
1054    }
1055
1056    /** @inheritDoc */
1057    protected function getExamplesMessages() {
1058        return [
1059            'action=query&meta=siteinfo&siprop=general|namespaces|namespacealiases|statistics'
1060                => 'apihelp-query+siteinfo-example-simple',
1061            'action=query&meta=siteinfo&siprop=interwikimap&sifilteriw=local'
1062                => 'apihelp-query+siteinfo-example-interwiki',
1063            'action=query&meta=siteinfo&siprop=dbrepllag&sishowalldb='
1064                => 'apihelp-query+siteinfo-example-replag',
1065        ];
1066    }
1067
1068    /** @inheritDoc */
1069    public function getHelpUrls() {
1070        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Siteinfo';
1071    }
1072}
1073
1074/** @deprecated class alias since 1.43 */
1075class_alias( ApiQuerySiteinfo::class, 'ApiQuerySiteinfo' );