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