Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.45% covered (warning)
86.45%
217 / 251
80.70% covered (warning)
80.70%
46 / 57
CRAP
0.00% covered (danger)
0.00%
0 / 1
SiteConfig
86.45% covered (warning)
86.45%
217 / 251
80.70% covered (warning)
80.70%
46 / 57
143.18
0.00% covered (danger)
0.00%
0 / 1
 __construct
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
5
 getObjectFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLogger
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 metrics
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 galleryOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 allowedExternalImagePrefixes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 determineArticlePath
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
 baseURI
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 relativeLinkPrefix
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 mwaToRegex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 redirectRegexp
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 categoryRegexp
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 bswRegexp
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 canonicalNamespaceId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 namespaceId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 namespaceName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 namespaceHasSubpages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 namespaceCase
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 namespaceIsTalk
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 ucfirst
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 specialPageLocalName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 interwikiMagic
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 magicLinkEnabled
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 interwikiMap
93.94% covered (success)
93.94%
31 / 33
0.00% covered (danger)
0.00%
0 / 1
11.03
 iwp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 legalTitleChars
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 linkPrefixRegex
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 linkTrail
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 langBcp47
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mainpage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mainPageLinkTarget
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMWConfigValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 rtl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 langConverterEnabledBcp47
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 script
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 scriptpath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 server
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 exportMetadataToHeadBcp47
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 timezoneOffset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 variants
91.30% covered (success)
91.30%
21 / 23
0.00% covered (danger)
0.00%
0 / 1
8.04
 variantsFor
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 widthOption
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getVariableIDs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFunctionSynonyms
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMagicWords
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMagicWordMatcher
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParameterizedAliasMatcher
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 populateExtensionTags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNonNativeExtensionTags
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getMaxTemplateDepth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMaxTemplateDepth
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getSpecialNSAliases
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 getSpecialPageAliases
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getProtocols
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNoFollowConfig
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getExternalLinkTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 supportsContentModel
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * Copyright (C) 2011-2022 Wikimedia Foundation and others.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20// NO_PRELOAD -- anonymous class in parent
21
22namespace MediaWiki\Parser\Parsoid\Config;
23
24use Language;
25use LanguageCode;
26use LanguageConverter;
27use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
28use MediaWiki\Config\Config;
29use MediaWiki\Config\MutableConfig;
30use MediaWiki\Config\ServiceOptions;
31use MediaWiki\Content\IContentHandlerFactory;
32use MediaWiki\Interwiki\InterwikiLookup;
33use MediaWiki\Languages\LanguageConverterFactory;
34use MediaWiki\Languages\LanguageFactory;
35use MediaWiki\Languages\LanguageNameUtils;
36use MediaWiki\Logger\LoggerFactory;
37use MediaWiki\MainConfigNames;
38use MediaWiki\Parser\MagicWordArray;
39use MediaWiki\Parser\MagicWordFactory;
40use MediaWiki\Parser\ParserOutput;
41use MediaWiki\SpecialPage\SpecialPageFactory;
42use MediaWiki\Title\NamespaceInfo;
43use MediaWiki\Title\Title;
44use MediaWiki\User\Options\UserOptionsLookup;
45use MediaWiki\Utils\UrlUtils;
46use MediaWiki\WikiMap\WikiMap;
47use MWUnknownContentModelException;
48use ParserFactory;
49use PrefixingStatsdDataFactoryProxy;
50use Psr\Log\LoggerInterface;
51use UnexpectedValueException;
52use Wikimedia\Bcp47Code\Bcp47Code;
53use Wikimedia\ObjectFactory\ObjectFactory;
54use Wikimedia\Parsoid\Config\SiteConfig as ISiteConfig;
55use Wikimedia\Parsoid\Core\ContentMetadataCollector;
56use Wikimedia\Parsoid\DOM\Document;
57use Wikimedia\Parsoid\Utils\Utils;
58
59/**
60 * Site-level configuration for Parsoid
61 *
62 * This includes both global configuration and wiki-level configuration.
63 *
64 * @since 1.39
65 * @internal
66 */
67class SiteConfig extends ISiteConfig {
68
69    /**
70     * Regular expression fragment for matching wikitext comments.
71     * Meant for inclusion in other regular expressions.
72     */
73    protected const COMMENT_REGEXP_FRAGMENT = '<!--(?>[\s\S]*?-->)';
74
75    public const CONSTRUCTOR_OPTIONS = [
76        MainConfigNames::GalleryOptions,
77        MainConfigNames::AllowExternalImages,
78        MainConfigNames::AllowExternalImagesFrom,
79        MainConfigNames::Server,
80        MainConfigNames::ArticlePath,
81        MainConfigNames::InterwikiMagic,
82        MainConfigNames::ExtraInterlanguageLinkPrefixes,
83        MainConfigNames::InterlanguageLinkCodeMap,
84        MainConfigNames::LocalInterwikis,
85        MainConfigNames::LanguageCode,
86        MainConfigNames::NamespaceAliases,
87        MainConfigNames::UrlProtocols,
88        MainConfigNames::Script,
89        MainConfigNames::ScriptPath,
90        MainConfigNames::LoadScript,
91        MainConfigNames::LocalTZoffset,
92        MainConfigNames::ThumbLimits,
93        MainConfigNames::MaxTemplateDepth,
94        MainConfigNames::NoFollowLinks,
95        MainConfigNames::NoFollowNsExceptions,
96        MainConfigNames::NoFollowDomainExceptions,
97        MainConfigNames::ExternalLinkTarget,
98        MainConfigNames::EnableMagicLinks,
99    ];
100
101    private ServiceOptions $config;
102    private Config $mwConfig;
103    /** Parsoid-specific options array from $config */
104    private array $parsoidSettings;
105    private Language $contLang;
106    private StatsdDataFactoryInterface $stats;
107    private MagicWordFactory $magicWordFactory;
108    private NamespaceInfo $namespaceInfo;
109    private SpecialPageFactory $specialPageFactory;
110    private InterwikiLookup $interwikiLookup;
111    private ParserFactory $parserFactory;
112    private UserOptionsLookup $userOptionsLookup;
113    private ObjectFactory $objectFactory;
114    private LanguageFactory $languageFactory;
115    private LanguageConverterFactory $languageConverterFactory;
116    private LanguageNameUtils $languageNameUtils;
117    private UrlUtils $urlUtils;
118    private IContentHandlerFactory $contentHandlerFactory;
119    private ?string $baseUri = null;
120    private ?string $relativeLinkPrefix = null;
121    private ?array $interwikiMap = null;
122    private ?array $variants = null;
123    private ?array $extensionTags = null;
124    private bool $isTimedMediaHandlerLoaded;
125
126    /**
127     * @param ServiceOptions $config MediaWiki main configuration object
128     * @param array $parsoidSettings Parsoid-specific options array from main configuration.
129     * @param ObjectFactory $objectFactory
130     * @param Language $contentLanguage Content language.
131     * @param StatsdDataFactoryInterface $stats
132     * @param MagicWordFactory $magicWordFactory
133     * @param NamespaceInfo $namespaceInfo
134     * @param SpecialPageFactory $specialPageFactory
135     * @param InterwikiLookup $interwikiLookup
136     * @param UserOptionsLookup $userOptionsLookup
137     * @param LanguageFactory $languageFactory
138     * @param LanguageConverterFactory $languageConverterFactory
139     * @param LanguageNameUtils $languageNameUtils
140     * @param UrlUtils $urlUtils
141     * @param IContentHandlerFactory $contentHandlerFactory
142     * @param array $extensionParsoidModules
143     * @param ParserFactory $parserFactory
144     * @param Config $mwConfig
145     * @param bool $isTimedMediaHandlerLoaded
146     */
147    public function __construct(
148        ServiceOptions $config,
149        array $parsoidSettings,
150        ObjectFactory $objectFactory,
151        Language $contentLanguage,
152        StatsdDataFactoryInterface $stats,
153        MagicWordFactory $magicWordFactory,
154        NamespaceInfo $namespaceInfo,
155        SpecialPageFactory $specialPageFactory,
156        InterwikiLookup $interwikiLookup,
157        UserOptionsLookup $userOptionsLookup,
158        LanguageFactory $languageFactory,
159        LanguageConverterFactory $languageConverterFactory,
160        LanguageNameUtils $languageNameUtils,
161        UrlUtils $urlUtils,
162        IContentHandlerFactory $contentHandlerFactory,
163        array $extensionParsoidModules,
164        // $parserFactory is temporary and may be removed once a better solution is found.
165        ParserFactory $parserFactory, // T268776
166        Config $mwConfig,
167        bool $isTimedMediaHandlerLoaded
168    ) {
169        parent::__construct();
170
171        $config->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
172        $this->config = $config;
173        $this->mwConfig = $mwConfig;
174        $this->parsoidSettings = $parsoidSettings;
175
176        $this->objectFactory = $objectFactory;
177        $this->contLang = $contentLanguage;
178        $this->stats = $stats;
179        $this->magicWordFactory = $magicWordFactory;
180        $this->namespaceInfo = $namespaceInfo;
181        $this->specialPageFactory = $specialPageFactory;
182        $this->interwikiLookup = $interwikiLookup;
183        $this->parserFactory = $parserFactory;
184        $this->userOptionsLookup = $userOptionsLookup;
185        $this->languageFactory = $languageFactory;
186        $this->languageConverterFactory = $languageConverterFactory;
187        $this->languageNameUtils = $languageNameUtils;
188        $this->urlUtils = $urlUtils;
189        $this->contentHandlerFactory = $contentHandlerFactory;
190
191        // Override parent default
192        if ( isset( $this->parsoidSettings['linting'] ) ) {
193            // @todo: Add this setting to MW's MainConfigSchema
194            $this->linterEnabled = $this->parsoidSettings['linting'];
195        }
196
197        if ( isset( $this->parsoidSettings['wt2htmlLimits'] ) ) {
198            $this->wt2htmlLimits = $this->parsoidSettings['wt2htmlLimits'] + $this->wt2htmlLimits;
199        }
200        if ( isset( $this->parsoidSettings['html2wtLimits'] ) ) {
201            $this->html2wtLimits = $this->parsoidSettings['html2wtLimits'] + $this->html2wtLimits;
202        }
203
204        // Register extension modules
205        foreach ( $extensionParsoidModules as $configOrSpec ) {
206            $this->registerExtensionModule( $configOrSpec );
207        }
208
209        $this->isTimedMediaHandlerLoaded = $isTimedMediaHandlerLoaded;
210    }
211
212    /** @inheritDoc */
213    public function getObjectFactory(): ObjectFactory {
214        return $this->objectFactory;
215    }
216
217    /** @inheritDoc */
218    public function getLogger(): LoggerInterface {
219        // TODO: inject
220        if ( $this->logger === null ) {
221            $this->logger = LoggerFactory::getInstance( 'Parsoid' );
222        }
223        return $this->logger;
224    }
225
226    public function metrics(): ?StatsdDataFactoryInterface {
227        // TODO: inject
228        static $prefixedMetrics = null;
229        if ( $prefixedMetrics === null ) {
230            $prefixedMetrics = new PrefixingStatsdDataFactoryProxy(
231                // Our stats will also get prefixed with 'MediaWiki.'
232                $this->stats,
233                $this->parsoidSettings['metricsPrefix'] ?? 'Parsoid.'
234            );
235        }
236        return $prefixedMetrics;
237    }
238
239    public function galleryOptions(): array {
240        return $this->config->get( MainConfigNames::GalleryOptions );
241    }
242
243    public function allowedExternalImagePrefixes(): array {
244        if ( $this->config->get( MainConfigNames::AllowExternalImages ) ) {
245            return [ '' ];
246        } else {
247            $allowFrom = $this->config->get( MainConfigNames::AllowExternalImagesFrom );
248            return $allowFrom ? (array)$allowFrom : [];
249        }
250    }
251
252    /**
253     * Determine the article base URI and relative prefix
254     *
255     * Populates `$this->baseUri` and `$this->relativeLinkPrefix` based on
256     * `$wgServer` and `$wgArticlePath`, by splitting it at the last '/' in the
257     * path portion.
258     */
259    private function determineArticlePath(): void {
260        $url = $this->config->get( MainConfigNames::Server ) .
261            $this->config->get( MainConfigNames::ArticlePath );
262
263        if ( substr( $url, -2 ) !== '$1' ) {
264            throw new UnexpectedValueException( "Article path '$url' does not have '$1' at the end" );
265        }
266        $url = substr( $url, 0, -2 );
267
268        $bits = $this->urlUtils->parse( $url );
269        if ( !$bits ) {
270            throw new UnexpectedValueException( "Failed to parse article path '$url'" );
271        }
272
273        if ( empty( $bits['path'] ) ) {
274            $path = '/';
275        } else {
276            $path = wfRemoveDotSegments( $bits['path'] );
277        }
278
279        $relParts = [ 'query' => true, 'fragment' => true ];
280        $base = array_diff_key( $bits, $relParts );
281        $rel = array_intersect_key( $bits, $relParts );
282
283        $i = strrpos( $path, '/' );
284        $base['path'] = substr( $path, 0, $i + 1 );
285        $rel['path'] = '.' . substr( $path, $i );
286
287        $this->baseUri = wfAssembleUrl( $base );
288        $this->relativeLinkPrefix = wfAssembleUrl( $rel );
289    }
290
291    public function baseURI(): string {
292        if ( $this->baseUri === null ) {
293            $this->determineArticlePath();
294        }
295        return $this->baseUri;
296    }
297
298    public function relativeLinkPrefix(): string {
299        if ( $this->relativeLinkPrefix === null ) {
300            $this->determineArticlePath();
301        }
302        return $this->relativeLinkPrefix;
303    }
304
305    /**
306     * This is very similar to MagicWordArray::getBaseRegex() except we
307     * don't emit the named grouping constructs, which can cause havoc
308     * when embedded in other regexps with grouping constructs.
309     *
310     * @param MagicWordArray $magicWordArray
311     * @param string $delimiter
312     * @return string
313     */
314    private static function mwaToRegex(
315        MagicWordArray $magicWordArray,
316        string $delimiter = '/'
317    ): string {
318        return implode( '|', $magicWordArray->getBaseRegex( false, $delimiter ) );
319    }
320
321    public function redirectRegexp(): string {
322        $redirect = self::mwaToRegex( $this->magicWordFactory->newArray( [ 'redirect' ] ), '@' );
323        return "@$redirect@Su";
324    }
325
326    public function categoryRegexp(): string {
327        $canon = $this->namespaceInfo->getCanonicalName( NS_CATEGORY );
328        $result = [ $canon ];
329        foreach ( $this->contLang->getNamespaceAliases() as $alias => $ns ) {
330            if ( $ns === NS_CATEGORY && $alias !== $canon ) {
331                $result[] = $alias;
332            }
333        }
334        $category = implode( '|', array_map( function ( $v ) {
335            return $this->quoteTitleRe( $v, '@' );
336        }, $result ) );
337        return "@(?i:$category)@";
338    }
339
340    public function bswRegexp(): string {
341        $bsw = self::mwaToRegex( $this->magicWordFactory->getDoubleUnderscoreArray(), '@' );
342        // Aliases for double underscore mws include the underscores
343        // So, strip them since the base regexp will have included them
344        // and they aren't expected at the use sites of bswRegexp
345        $bsw = str_replace( '__', '', $bsw );
346        return "@$bsw@Su";
347    }
348
349    /** @inheritDoc */
350    public function canonicalNamespaceId( string $name ): ?int {
351        $ret = $this->namespaceInfo->getCanonicalIndex( $name );
352        return $ret === false ? null : $ret;
353    }
354
355    /** @inheritDoc */
356    public function namespaceId( string $name ): ?int {
357        $ret = $this->contLang->getNsIndex( $name );
358        return $ret === false ? null : $ret;
359    }
360
361    /** @inheritDoc */
362    public function namespaceName( int $ns ): ?string {
363        $ret = $this->contLang->getFormattedNsText( $ns );
364        return $ret === '' && $ns !== NS_MAIN ? null : $ret;
365    }
366
367    /** @inheritDoc */
368    public function namespaceHasSubpages( int $ns ): bool {
369        return $this->namespaceInfo->hasSubpages( $ns );
370    }
371
372    /** @inheritDoc */
373    public function namespaceCase( int $ns ): string {
374        return $this->namespaceInfo->isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive';
375    }
376
377    /** @inheritDoc */
378    public function namespaceIsTalk( int $ns ): bool {
379        return $this->namespaceInfo->isTalk( $ns );
380    }
381
382    /** @inheritDoc */
383    public function ucfirst( string $str ): string {
384        return $this->contLang->ucfirst( $str );
385    }
386
387    /** @inheritDoc */
388    public function specialPageLocalName( string $alias ): ?string {
389        $aliases = $this->specialPageFactory->resolveAlias( $alias );
390        return $aliases[0] !== null ? $this->specialPageFactory->getLocalNameFor( ...$aliases ) : $alias;
391    }
392
393    public function interwikiMagic(): bool {
394        return $this->config->get( MainConfigNames::InterwikiMagic );
395    }
396
397    /** @inheritDoc */
398    public function magicLinkEnabled( string $which ): bool {
399        $m = $this->config->get( MainConfigNames::EnableMagicLinks );
400        return $m[$which] ?? true;
401    }
402
403    public function interwikiMap(): array {
404        // Unfortunate that this mostly duplicates \ApiQuerySiteinfo::appendInterwikiMap()
405        if ( $this->interwikiMap !== null ) {
406            return $this->interwikiMap;
407        }
408        $this->interwikiMap = [];
409
410        $getPrefixes = $this->interwikiLookup->getAllPrefixes();
411        $langNames = $this->languageNameUtils->getLanguageNames();
412        $extraLangPrefixes = $this->config->get( MainConfigNames::ExtraInterlanguageLinkPrefixes );
413        $extraLangCodeMap = $this->config->get( MainConfigNames::InterlanguageLinkCodeMap );
414        $localInterwikis = $this->config->get( MainConfigNames::LocalInterwikis );
415
416        foreach ( $getPrefixes as $row ) {
417            $prefix = $row['iw_prefix'];
418            $val = [];
419            $val['prefix'] = $prefix;
420            // ApiQuerySiteInfo::appendInterwikiMap uses PROTO_CURRENT here,
421            // but that's the 'current' protocol *of the API request*; use
422            // PROTO_CANONICAL instead.
423            $val['url'] = $this->urlUtils->expand( $row['iw_url'], PROTO_CANONICAL ) ?? false;
424
425            // Fix up broken interwiki hrefs that are missing a $1 placeholder
426            // Just append the placeholder at the end.
427            // This makes sure that the interwikiMatcher adds one match
428            // group per URI, and that interwiki links work as expected.
429            if ( !str_contains( $val['url'], '$1' ) ) {
430                $val['url'] .= '$1';
431            }
432
433            if ( str_starts_with( $row['iw_url'], '//' ) ) {
434                $val['protorel'] = true;
435            }
436            if ( isset( $row['iw_local'] ) && $row['iw_local'] == '1' ) {
437                $val['local'] = true;
438            }
439            if ( isset( $langNames[$prefix] ) ) {
440                $val['language'] = true;
441                $standard = LanguageCode::replaceDeprecatedCodes( $prefix );
442                if ( $standard !== $prefix ) {
443                    # Note that even if this code is deprecated, it should
444                    # only be remapped if extralanglink (set below) is false.
445                    $val['deprecated'] = $standard;
446                }
447                $val['bcp47'] = LanguageCode::bcp47( $standard );
448            }
449            if ( in_array( $prefix, $localInterwikis, true ) ) {
450                $val['localinterwiki'] = true;
451            }
452            if ( in_array( $prefix, $extraLangPrefixes, true ) ) {
453                $val['extralanglink'] = true;
454                $val['code'] = $extraLangCodeMap[$prefix] ?? $prefix;
455                $val['bcp47'] = LanguageCode::bcp47( $val['code'] );
456            }
457
458            $this->interwikiMap[$prefix] = $val;
459        }
460        return $this->interwikiMap;
461    }
462
463    public function iwp(): string {
464        return WikiMap::getCurrentWikiId();
465    }
466
467    public function legalTitleChars(): string {
468        return Title::legalChars();
469    }
470
471    public function linkPrefixRegex(): ?string {
472        if ( !$this->contLang->linkPrefixExtension() ) {
473            return null;
474        }
475        return '/[' . $this->contLang->linkPrefixCharset() . ']+$/Du';
476    }
477
478    /** @inheritDoc */
479    protected function linkTrail(): string {
480        return $this->contLang->linkTrail();
481    }
482
483    public function langBcp47(): Bcp47Code {
484        return $this->contLang;
485    }
486
487    public function mainpage(): string {
488        // @todo Perhaps should inject TitleFactory here?
489        return Title::newMainPage()->getPrefixedText();
490    }
491
492    public function mainPageLinkTarget(): Title {
493        // @todo Perhaps should inject TitleFactory here?
494        return Title::newMainPage();
495    }
496
497    /**
498     * Lookup config
499     * @param string $key
500     * @return mixed config value for $key, if present or null, if not.
501     */
502    public function getMWConfigValue( string $key ) {
503        return $this->mwConfig->has( $key ) ? $this->mwConfig->get( $key ) : null;
504    }
505
506    public function rtl(): bool {
507        return $this->contLang->isRTL();
508    }
509
510    /**
511     * @param Bcp47Code $lang
512     * @return bool
513     */
514    public function langConverterEnabledBcp47( Bcp47Code $lang ): bool {
515        if ( $this->languageConverterFactory->isConversionDisabled() ) {
516            return false;
517        }
518
519        $langObject = $this->languageFactory->getLanguage( $lang );
520        if ( !in_array( $langObject->getCode(), LanguageConverter::$languagesWithVariants, true ) ) {
521            return false;
522        }
523        $converter = $this->languageConverterFactory->getLanguageConverter( $langObject );
524        return $converter->hasVariants();
525    }
526
527    public function script(): string {
528        return $this->config->get( MainConfigNames::Script );
529    }
530
531    public function scriptpath(): string {
532        return $this->config->get( MainConfigNames::ScriptPath );
533    }
534
535    public function server(): string {
536        return $this->config->get( MainConfigNames::Server );
537    }
538
539    /**
540     * @inheritDoc
541     * @param Document $document
542     * @param ContentMetadataCollector $metadata
543     * @param string $defaultTitle
544     * @param Bcp47Code $lang
545     */
546    public function exportMetadataToHeadBcp47(
547        Document $document,
548        ContentMetadataCollector $metadata,
549        string $defaultTitle,
550        Bcp47Code $lang
551    ): void {
552        '@phan-var ParserOutput $metadata'; // @var ParserOutput $metadata
553        // Look for a displaytitle.
554        $displayTitle = $metadata->getPageProperty( 'displaytitle' ) ?:
555            // Use the default title, properly escaped
556            Utils::escapeHtml( $defaultTitle );
557        $this->exportMetadataHelper(
558            $document,
559            $this->config->get( MainConfigNames::LoadScript ),
560            $metadata->getModules(),
561            $metadata->getModuleStyles(),
562            $metadata->getJsConfigVars(),
563            $displayTitle,
564            $lang
565        );
566    }
567
568    public function timezoneOffset(): int {
569        return $this->config->get( MainConfigNames::LocalTZoffset );
570    }
571
572    /**
573     * Language variant information
574     * @return array<string,array> Keys are MediaWiki-internal variant codes (e.g. "zh-cn"),
575     * values are arrays with two fields:
576     *   - base: (string) Base language code (e.g. "zh") (MediaWiki-internal)
577     *   - fallbacks: (string[]) Fallback variants (MediaWiki-internal codes)
578     * @deprecated since 1.43; use ::variantsFor() (T320662)
579     */
580    public function variants(): array {
581        // Deprecated for all external callers; to make private and remove this warning.
582        if ( wfGetCaller() !== __CLASS__ . '->variantsFor' ) {
583            wfDeprecated( __METHOD__, '1.43' );
584        }
585
586        if ( $this->variants !== null ) {
587            return $this->variants;
588        }
589        $this->variants = [];
590
591        $langNames = LanguageConverter::$languagesWithVariants;
592        if ( $this->languageConverterFactory->isConversionDisabled() ) {
593            // Ensure result is empty if language conversion is disabled.
594            $langNames = [];
595        }
596
597        foreach ( $langNames as $langCode ) {
598            $lang = $this->languageFactory->getLanguage( $langCode );
599            $converter = $this->languageConverterFactory->getLanguageConverter( $lang );
600            if ( !$converter->hasVariants() ) {
601                continue;
602            }
603
604            $variants = $converter->getVariants();
605            foreach ( $variants as $v ) {
606                $fallbacks = $converter->getVariantFallbacks( $v );
607                if ( !is_array( $fallbacks ) ) {
608                    $fallbacks = [ $fallbacks ];
609                }
610                $this->variants[$v] = [
611                    'base' => $langCode,
612                    'fallbacks' => $fallbacks,
613                ];
614            }
615        }
616        return $this->variants;
617    }
618
619    /**
620     * Language variant information for the given language (or null if
621     * unknown).
622     * @param Bcp47Code $code The language for which you want variant information
623     * @return ?array{base:Bcp47Code,fallbacks:Bcp47Code[]} an array with
624     * two fields:
625     *   - base: (Bcp47Code) Base BCP-47 language code (e.g. "zh")
626     *   - fallbacks: (Bcp47Code[]) Fallback variants, as BCP-47 codes
627     */
628    public function variantsFor( Bcp47Code $code ): ?array {
629        $variants = $this->variants();
630        $lang = $this->languageFactory->getLanguage( $code );
631        $tuple = $variants[$lang->getCode()] ?? null;
632        if ( $tuple === null ) {
633            return null;
634        }
635        return [
636            'base' => $this->languageFactory->getLanguage( $tuple['base'] ),
637            'fallbacks' => array_map(
638                [ $this->languageFactory, 'getLanguage' ],
639                $tuple['fallbacks']
640            ),
641        ];
642    }
643
644    public function widthOption(): int {
645        // Even though this looks like Parsoid is supporting per-user thumbsize
646        // options, that is not the case, Parsoid doesn't receive user session state
647        $thumbsize = $this->userOptionsLookup->getDefaultOption( 'thumbsize' );
648        return $this->config->get( MainConfigNames::ThumbLimits )[$thumbsize];
649    }
650
651    /** @inheritDoc */
652    protected function getVariableIDs(): array {
653        return $this->magicWordFactory->getVariableIDs();
654    }
655
656    /** @inheritDoc */
657    protected function getFunctionSynonyms(): array {
658        return $this->parserFactory->getMainInstance()->getFunctionSynonyms();
659    }
660
661    /** @return array<string,array> $magicWord => [ int $caseSensitive, string ...$alias ] */
662    protected function getMagicWords(): array {
663        return $this->contLang->getMagicWords();
664    }
665
666    /** @inheritDoc */
667    public function getMagicWordMatcher( string $id ): string {
668        return $this->magicWordFactory->get( $id )->getRegexStartToEnd();
669    }
670
671    /** @inheritDoc */
672    public function getParameterizedAliasMatcher( array $words ): callable {
673        // PORT-FIXME: this should be combined with
674        // getMediaPrefixParameterizedAliasMatcher; see PORT-FIXME comment
675        // in that method.
676        // Filter out timedmedia-* unless that extension is loaded, so Parsoid
677        // doesn't have a hard dependency on an extension.
678        if ( !$this->isTimedMediaHandlerLoaded ) {
679            $words = preg_grep( '/^timedmedia_/', $words, PREG_GREP_INVERT );
680        }
681        $words = $this->magicWordFactory->newArray( $words );
682        return static function ( $text ) use ( $words ) {
683            $ret = $words->matchVariableStartToEnd( $text );
684            if ( $ret[0] === false || $ret[1] === false ) {
685                return null;
686            } else {
687                return [ 'k' => $ret[0], 'v' => $ret[1] ];
688            }
689        };
690    }
691
692    private function populateExtensionTags(): void {
693        $this->extensionTags = array_fill_keys( $this->parserFactory->getMainInstance()->getTags(), true );
694    }
695
696    /** @inheritDoc */
697    protected function getNonNativeExtensionTags(): array {
698        if ( $this->extensionTags === null ) {
699            $this->populateExtensionTags();
700        }
701        return $this->extensionTags;
702    }
703
704    /** @inheritDoc */
705    public function getMaxTemplateDepth(): int {
706        return (int)$this->config->get( MainConfigNames::MaxTemplateDepth );
707    }
708
709    /**
710     * Overrides the max template depth in the MediaWiki configuration.
711     * @param int $depth
712     */
713    public function setMaxTemplateDepth( int $depth ): void {
714        // Parsoid's command-line tools let you set the max template depth
715        // as a CLI argument.  Since we currently invoke the legacy
716        // preprocessor in some situations, we can't just override
717        // ::getMaxTemplateDepth() above, we need to reset the Config
718        // service.
719        if ( $this->config instanceof MutableConfig ) {
720            $this->config->set( MainConfigNames::MaxTemplateDepth, $depth );
721        } else {
722            // Fall back on global variable (hopefully we're using
723            // a GlobalVarConfig and this will work)
724            $GLOBALS['wgMaxTemplateDepth'] = $depth;
725        }
726    }
727
728    /** @inheritDoc */
729    protected function getSpecialNSAliases(): array {
730        $nsAliases = [
731            'Special',
732            $this->quoteTitleRe( $this->contLang->getNsText( NS_SPECIAL ) )
733        ];
734        foreach (
735            $this->contLang->getNamespaceAliases() +
736            $this->config->get( MainConfigNames::NamespaceAliases )
737            as $name => $ns
738        ) {
739            if ( $ns === NS_SPECIAL ) {
740                $nsAliases[] = $this->quoteTitleRe( $name );
741            }
742        }
743
744        return $nsAliases;
745    }
746
747    /** @inheritDoc */
748    protected function getSpecialPageAliases( string $specialPage ): array {
749        return array_merge( [ $specialPage ],
750            $this->contLang->getSpecialPageAliases()[$specialPage] ?? []
751        );
752    }
753
754    /** @inheritDoc */
755    protected function getProtocols(): array {
756        return $this->config->get( MainConfigNames::UrlProtocols );
757    }
758
759    /** @return array */
760    public function getNoFollowConfig(): array {
761        return [
762            'nofollow' => $this->config->get( MainConfigNames::NoFollowLinks ),
763            'nsexceptions' => $this->config->get( MainConfigNames::NoFollowNsExceptions ),
764            'domainexceptions' => $this->config->get( MainConfigNames::NoFollowDomainExceptions )
765        ];
766    }
767
768    /** @return string|false */
769    public function getExternalLinkTarget() {
770        return $this->config->get( MainConfigNames::ExternalLinkTarget );
771    }
772
773    // MW-specific helper
774
775    /**
776     * Returns true iff Parsoid natively supports the given content model.
777     * @param string $model content model identifier
778     * @return bool
779     */
780    public function supportsContentModel( string $model ): bool {
781        if ( $model === CONTENT_MODEL_WIKITEXT ) {
782            return true;
783        }
784
785        // Check if the content model serializes to wikitext.
786        // NOTE: We could use isSupportedFormat( CONTENT_FORMAT_WIKITEXT ) if PageContent::getContent()
787        //       would specify the format when calling serialize().
788        try {
789            $handler = $this->contentHandlerFactory->getContentHandler( $model );
790            if ( $handler->getDefaultFormat() === CONTENT_FORMAT_WIKITEXT ) {
791                return true;
792            }
793        } catch ( MWUnknownContentModelException $ex ) {
794            // If the content model is not known, it can't be supported.
795            return false;
796        }
797
798        return $this->getContentModelHandler( $model ) !== null;
799    }
800
801}