Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.77% covered (warning)
70.77%
494 / 698
36.36% covered (danger)
36.36%
32 / 88
CRAP
0.00% covered (danger)
0.00%
0 / 1
CoreParserFunctions
70.88% covered (warning)
70.88%
494 / 697
36.36% covered (danger)
36.36%
32 / 88
2321.95
0.00% covered (danger)
0.00%
0 / 1
 register
90.74% covered (success)
90.74%
49 / 54
0.00% covered (danger)
0.00%
0 / 1
4.01
 intFunction
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 formatDate
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 ns
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 nse
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 urlencode
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 lcfirst
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
 lc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 uc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 localurl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 localurle
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 fullurl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fullurle
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 canonicalurl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canonicalurle
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 urlFunction
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
4.59
 formatnum
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getLegacyFormatNum
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 grammar
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 gender
77.78% covered (warning)
77.78%
14 / 18
0.00% covered (danger)
0.00%
0 / 1
8.70
 plural
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 formal
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 bidi
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 displaytitle
81.25% covered (warning)
81.25%
39 / 48
0.00% covered (danger)
0.00%
0 / 1
15.29
 matchAgainstMagicword
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 formatRaw
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
12.41
 numberofpages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 numberofusers
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 numberofactiveusers
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 numberofarticles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 numberoffiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 numberofadmins
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 numberofedits
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pagesinnamespace
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 numberingroup
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 makeTitle
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 namespace
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 namespacee
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 namespacenumber
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 talkspace
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 talkspacee
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 subjectspace
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 subjectspacee
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 pagename
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 pagenamee
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 fullpagename
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 fullpagenamee
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 subpagename
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 subpagenamee
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 rootpagename
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 rootpagenamee
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 basepagename
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 basepagenamee
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 talkpagename
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 talkpagenamee
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 subjectpagename
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 subjectpagenamee
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 pagesincategory
44.74% covered (danger)
44.74%
17 / 38
0.00% covered (danger)
0.00%
0 / 1
15.27
 pagesize
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
 protectionlevel
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 protectionexpiry
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 language
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 dir
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 bcp47
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 pad
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
5.01
 padleft
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 padright
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 anchorencode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 special
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 speciale
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 defaultsort
55.00% covered (warning)
55.00%
11 / 20
0.00% covered (danger)
0.00%
0 / 1
13.83
 filepath
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 tagObj
62.50% covered (warning)
62.50%
20 / 32
0.00% covered (danger)
0.00%
0 / 1
15.27
 getCachedRevisionObject
83.33% covered (warning)
83.33%
25 / 30
0.00% covered (danger)
0.00%
0 / 1
16.04
 pageid
43.48% covered (danger)
43.48%
10 / 23
0.00% covered (danger)
0.00%
0 / 1
23.63
 revisionid
61.54% covered (warning)
61.54%
16 / 26
0.00% covered (danger)
0.00%
0 / 1
30.57
 getRevisionTimestampSubstring
77.78% covered (warning)
77.78%
14 / 18
0.00% covered (danger)
0.00%
0 / 1
6.40
 revisionday
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 revisionday2
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 revisionmonth
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 revisionmonth1
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 revisionyear
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 revisiontimestamp
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 revisionuser
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 cascadingsources
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 interwikilink
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 interlanguagelink
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
5.01
1<?php
2/**
3 * Parser functions provided by MediaWiki core
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 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Parser
22 */
23
24namespace MediaWiki\Parser;
25
26use InvalidArgumentException;
27use MediaWiki\Category\Category;
28use MediaWiki\Config\ServiceOptions;
29use MediaWiki\Language\Language;
30use MediaWiki\Language\LanguageCode;
31use MediaWiki\Languages\LanguageNameUtils;
32use MediaWiki\Linker\Linker;
33use MediaWiki\MainConfigNames;
34use MediaWiki\MediaWikiServices;
35use MediaWiki\Message\Message;
36use MediaWiki\Revision\RevisionAccessException;
37use MediaWiki\Revision\RevisionRecord;
38use MediaWiki\SiteStats\SiteStats;
39use MediaWiki\SpecialPage\SpecialPage;
40use MediaWiki\Title\Title;
41use MediaWiki\Title\TitleValue;
42use MediaWiki\User\User;
43use Wikimedia\Bcp47Code\Bcp47CodeValue;
44use Wikimedia\RemexHtml\Tokenizer\Attributes;
45use Wikimedia\RemexHtml\Tokenizer\PlainAttributes;
46
47/**
48 * Various core parser functions, registered in every Parser
49 * @ingroup Parser
50 */
51class CoreParserFunctions {
52    /** @var int Assume that no output will later be saved this many seconds after parsing */
53    private const MAX_TTS = 900;
54
55    /**
56     * @internal
57     */
58    public const REGISTER_OPTIONS = [
59        // See documentation for the corresponding config options
60        MainConfigNames::AllowDisplayTitle,
61        MainConfigNames::AllowSlowParserFunctions,
62    ];
63
64    /**
65     * @param Parser $parser
66     * @param ServiceOptions $options
67     *
68     * @return void
69     * @internal
70     */
71    public static function register( Parser $parser, ServiceOptions $options ) {
72        $options->assertRequiredOptions( self::REGISTER_OPTIONS );
73        $allowDisplayTitle = $options->get( MainConfigNames::AllowDisplayTitle );
74        $allowSlowParserFunctions = $options->get( MainConfigNames::AllowSlowParserFunctions );
75
76        # Syntax for arguments (see Parser::setFunctionHook):
77        #  "name for lookup in localized magic words array",
78        #  function callback,
79        #  optional Parser::SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}}
80        #    instead of {{#int:...}})
81        $noHashFunctions = [
82            'ns', 'nse', 'urlencode', 'lcfirst', 'ucfirst', 'lc', 'uc',
83            'localurl', 'localurle', 'fullurl', 'fullurle', 'canonicalurl',
84            'canonicalurle', 'formatnum', 'grammar', 'gender', 'plural', 'formal',
85            'bidi', 'numberingroup', 'language',
86            'padleft', 'padright', 'anchorencode', 'defaultsort', 'filepath',
87            'pagesincategory', 'pagesize', 'protectionlevel', 'protectionexpiry',
88            # The following are the "parser function" forms of magic
89            # variables defined in CoreMagicVariables.  The no-args form will
90            # go through the magic variable code path (and be cached); the
91            # presence of arguments will cause the parser function form to
92            # be invoked. (Note that the actual implementation will pass
93            # a Parser object as first argument, in addition to the
94            # parser function parameters.)
95
96            # For this group, the first parameter to the parser function is
97            # "page title", and the no-args form (and the magic variable)
98            # defaults to "current page title".
99            'pagename', 'pagenamee',
100            'fullpagename', 'fullpagenamee',
101            'subpagename', 'subpagenamee',
102            'rootpagename', 'rootpagenamee',
103            'basepagename', 'basepagenamee',
104            'talkpagename', 'talkpagenamee',
105            'subjectpagename', 'subjectpagenamee',
106            'pageid', 'revisionid', 'revisionday',
107            'revisionday2', 'revisionmonth', 'revisionmonth1', 'revisionyear',
108            'revisiontimestamp',
109            'revisionuser',
110            'cascadingsources',
111            'namespace', 'namespacee', 'namespacenumber', 'talkspace', 'talkspacee',
112            'subjectspace', 'subjectspacee',
113
114            # More parser functions corresponding to CoreMagicVariables.
115            # For this group, the first parameter to the parser function is
116            # "raw" (uses the 'raw' format if present) and the no-args form
117            # (and the magic variable) defaults to 'not raw'.
118            'numberofarticles', 'numberoffiles',
119            'numberofusers',
120            'numberofactiveusers',
121            'numberofpages',
122            'numberofadmins',
123            'numberofedits',
124
125            # These magic words already contain the hash, and the no-args form
126            # is the same as passing an empty first argument
127            'bcp47',
128            'dir',
129            'interwikilink',
130            'interlanguagelink',
131        ];
132        foreach ( $noHashFunctions as $func ) {
133            $parser->setFunctionHook( $func, [ __CLASS__, $func ], Parser::SFH_NO_HASH );
134        }
135
136        $parser->setFunctionHook( 'int', [ __CLASS__, 'intFunction' ], Parser::SFH_NO_HASH );
137        $parser->setFunctionHook( 'special', [ __CLASS__, 'special' ] );
138        $parser->setFunctionHook( 'speciale', [ __CLASS__, 'speciale' ] );
139        $parser->setFunctionHook( 'tag', [ __CLASS__, 'tagObj' ], Parser::SFH_OBJECT_ARGS );
140        $parser->setFunctionHook( 'formatdate', [ __CLASS__, 'formatDate' ] );
141
142        if ( $allowDisplayTitle ) {
143            $parser->setFunctionHook(
144                'displaytitle',
145                [ __CLASS__, 'displaytitle' ],
146                Parser::SFH_NO_HASH
147            );
148        }
149        if ( $allowSlowParserFunctions ) {
150            $parser->setFunctionHook(
151                'pagesinnamespace',
152                [ __CLASS__, 'pagesinnamespace' ],
153                Parser::SFH_NO_HASH
154            );
155        }
156    }
157
158    /**
159     * @param Parser $parser
160     * @param string $part1 Message key
161     * @param string ...$params To pass to wfMessage()
162     * @return array
163     */
164    public static function intFunction( $parser, $part1 = '', ...$params ) {
165        if ( strval( $part1 ) !== '' ) {
166            $message = wfMessage( $part1, $params )
167                ->inLanguage( $parser->getOptions()->getUserLangObj() );
168            return [ $message->plain(), 'noparse' => false ];
169        } else {
170            return [ 'found' => false ];
171        }
172    }
173
174    /**
175     * @param Parser $parser
176     * @param string $date
177     * @param string|null $defaultPref
178     *
179     * @return string
180     */
181    public static function formatDate( $parser, $date, $defaultPref = null ) {
182        $lang = $parser->getTargetLanguage();
183        $df = MediaWikiServices::getInstance()->getDateFormatterFactory()->get( $lang );
184
185        $date = trim( $date );
186
187        $pref = $parser->getOptions()->getDateFormat();
188
189        // Specify a different default date format other than the normal default
190        // if the user has 'default' for their setting
191        if ( $pref == 'default' && $defaultPref ) {
192            $pref = $defaultPref;
193        }
194
195        $date = $df->reformat( $pref, $date, [ 'match-whole' ] );
196        return $date;
197    }
198
199    public static function ns( $parser, $part1 = '' ) {
200        if ( intval( $part1 ) || $part1 == "0" ) {
201            $index = intval( $part1 );
202        } else {
203            $index = $parser->getContentLanguage()->getNsIndex( str_replace( ' ', '_', $part1 ) );
204        }
205        if ( $index !== false ) {
206            return $parser->getContentLanguage()->getFormattedNsText( $index );
207        } else {
208            return [ 'found' => false ];
209        }
210    }
211
212    public static function nse( $parser, $part1 = '' ) {
213        $ret = self::ns( $parser, $part1 );
214        if ( is_string( $ret ) ) {
215            $ret = wfUrlencode( str_replace( ' ', '_', $ret ) );
216        }
217        return $ret;
218    }
219
220    /**
221     * urlencodes a string according to one of three patterns: (T24474)
222     *
223     * By default (for HTTP "query" strings), spaces are encoded as '+'.
224     * Or to encode a value for the HTTP "path", spaces are encoded as '%20'.
225     * For links to "wiki"s, or similar software, spaces are encoded as '_',
226     *
227     * @param Parser $parser
228     * @param string $s The text to encode.
229     * @param string|null $arg (optional): The type of encoding.
230     * @return string
231     */
232    public static function urlencode( $parser, $s = '', $arg = null ) {
233        static $magicWords = null;
234        if ( $magicWords === null ) {
235            $magicWords =
236                $parser->getMagicWordFactory()->newArray( [ 'url_path', 'url_query', 'url_wiki' ] );
237        }
238        switch ( $magicWords->matchStartToEnd( $arg ?? '' ) ) {
239            // Encode as though it's a wiki page, '_' for ' '.
240            case 'url_wiki':
241                $func = 'wfUrlencode';
242                $s = str_replace( ' ', '_', $s );
243                break;
244
245            // Encode for an HTTP Path, '%20' for ' '.
246            case 'url_path':
247                $func = 'rawurlencode';
248                break;
249
250            // Encode for HTTP query, '+' for ' '.
251            case 'url_query':
252            default:
253                $func = 'urlencode';
254        }
255        // See T105242, where the choice to kill markers and various
256        // other options were discussed.
257        return $func( $parser->killMarkers( $s ) );
258    }
259
260    public static function lcfirst( $parser, $s = '' ) {
261        return $parser->getContentLanguage()->lcfirst( $s );
262    }
263
264    public static function ucfirst( $parser, $s = '' ) {
265        return $parser->getContentLanguage()->ucfirst( $s );
266    }
267
268    /**
269     * @param Parser $parser
270     * @param string $s
271     * @return string
272     */
273    public static function lc( $parser, $s = '' ) {
274        return $parser->markerSkipCallback( $s, [ $parser->getContentLanguage(), 'lc' ] );
275    }
276
277    /**
278     * @param Parser $parser
279     * @param string $s
280     * @return string
281     */
282    public static function uc( $parser, $s = '' ) {
283        return $parser->markerSkipCallback( $s, [ $parser->getContentLanguage(), 'uc' ] );
284    }
285
286    public static function localurl( $parser, $s = '', $arg = null ) {
287        return self::urlFunction( 'getLocalURL', $s, $arg );
288    }
289
290    public static function localurle( $parser, $s = '', $arg = null ) {
291        $temp = self::urlFunction( 'getLocalURL', $s, $arg );
292        if ( !is_string( $temp ) ) {
293            return $temp;
294        } else {
295            return htmlspecialchars( $temp, ENT_COMPAT );
296        }
297    }
298
299    public static function fullurl( $parser, $s = '', $arg = null ) {
300        return self::urlFunction( 'getFullURL', $s, $arg );
301    }
302
303    public static function fullurle( $parser, $s = '', $arg = null ) {
304        $temp = self::urlFunction( 'getFullURL', $s, $arg );
305        if ( !is_string( $temp ) ) {
306            return $temp;
307        } else {
308            return htmlspecialchars( $temp, ENT_COMPAT );
309        }
310    }
311
312    public static function canonicalurl( $parser, $s = '', $arg = null ) {
313        return self::urlFunction( 'getCanonicalURL', $s, $arg );
314    }
315
316    public static function canonicalurle( $parser, $s = '', $arg = null ) {
317        $temp = self::urlFunction( 'getCanonicalURL', $s, $arg );
318        if ( !is_string( $temp ) ) {
319            return $temp;
320        } else {
321            return htmlspecialchars( $temp, ENT_COMPAT );
322        }
323    }
324
325    public static function urlFunction( $func, $s = '', $arg = null ) {
326        # Due to order of execution of a lot of bits, the values might be encoded
327        # before arriving here; if that's true, then the title can't be created
328        # and the variable will fail. If we can't get a decent title from the first
329        # attempt, url-decode and try for a second.
330        $title = Title::newFromText( $s ) ?? Title::newFromURL( urldecode( $s ) );
331        if ( $title !== null ) {
332            # Convert NS_MEDIA -> NS_FILE
333            if ( $title->inNamespace( NS_MEDIA ) ) {
334                $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
335            }
336            if ( $arg !== null ) {
337                $text = $title->$func( $arg );
338            } else {
339                $text = $title->$func();
340            }
341            return $text;
342        } else {
343            return [ 'found' => false ];
344        }
345    }
346
347    /**
348     * @param Parser $parser
349     * @param string $num
350     * @param string|null $arg
351     * @return string
352     */
353    public static function formatnum( $parser, $num = '', $arg = null ) {
354        if ( self::matchAgainstMagicword( $parser->getMagicWordFactory(), 'rawsuffix', $arg ) ) {
355            $func = [ $parser->getTargetLanguage(), 'parseFormattedNumber' ];
356        } elseif (
357            self::matchAgainstMagicword( $parser->getMagicWordFactory(), 'nocommafysuffix', $arg )
358        ) {
359            $func = [ $parser->getTargetLanguage(), 'formatNumNoSeparators' ];
360            $func = self::getLegacyFormatNum( $parser, $func );
361        } else {
362            $func = [ $parser->getTargetLanguage(), 'formatNum' ];
363            $func = self::getLegacyFormatNum( $parser, $func );
364        }
365        return $parser->markerSkipCallback( $num, $func );
366    }
367
368    /**
369     * @param Parser $parser
370     * @param callable $callback
371     *
372     * @return callable
373     */
374    private static function getLegacyFormatNum( $parser, $callback ) {
375        // For historic reasons, the formatNum parser function will
376        // take arguments which are not actually formatted numbers,
377        // which then trigger deprecation warnings in Language::formatNum*.
378        // Instead emit a tracking category instead to allow linting.
379        return static function ( $number ) use ( $parser, $callback ) {
380            $validNumberRe = '(-(?=[\d\.]))?(\d+|(?=\.\d))(\.\d*)?([Ee][-+]?\d+)?';
381            if (
382                !is_numeric( $number ) &&
383                $number !== (string)NAN &&
384                $number !== (string)INF &&
385                $number !== (string)-INF
386            ) {
387                $parser->addTrackingCategory( 'nonnumeric-formatnum' );
388                // Don't split on NAN/INF in the legacy case since they are
389                // likely to be found embedded inside non-numeric text.
390                return preg_replace_callback( "/{$validNumberRe}/", static function ( $m ) use ( $callback ) {
391                    return call_user_func( $callback, $m[0] );
392                }, $number );
393            }
394            return call_user_func( $callback, $number );
395        };
396    }
397
398    /**
399     * @param Parser $parser
400     * @param string $case
401     * @param string $word
402     * @return string
403     */
404    public static function grammar( $parser, $case = '', $word = '' ) {
405        $word = $parser->killMarkers( $word );
406        return $parser->getTargetLanguage()->convertGrammar( $word, $case );
407    }
408
409    /**
410     * @param Parser $parser
411     * @param string $username
412     * @param string ...$forms What to output for each gender
413     * @return string
414     */
415    public static function gender( $parser, $username, ...$forms ) {
416        // Some shortcuts to avoid loading user data unnecessarily
417        if ( count( $forms ) === 0 ) {
418            return '';
419        } elseif ( count( $forms ) === 1 ) {
420            return $forms[0];
421        }
422
423        $username = trim( $username );
424
425        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
426        $gender = $userOptionsLookup->getDefaultOption( 'gender' );
427
428        // allow prefix and normalize (e.g. "&#42;foo" -> "*foo" ).
429        $title = Title::newFromText( $username, NS_USER );
430
431        if ( $title && $title->inNamespace( NS_USER ) ) {
432            $username = $title->getText();
433        }
434
435        // check parameter, or use the ParserOptions if in interface message
436        $user = User::newFromName( $username );
437        $genderCache = MediaWikiServices::getInstance()->getGenderCache();
438        if ( $user ) {
439            $gender = $genderCache->getGenderOf( $user, __METHOD__ );
440        } elseif ( $username === '' && $parser->getOptions()->getInterfaceMessage() ) {
441            $gender = $genderCache->getGenderOf( $parser->getOptions()->getUserIdentity(), __METHOD__ );
442        }
443        $ret = $parser->getTargetLanguage()->gender( $gender, $forms );
444        return $ret;
445    }
446
447    /**
448     * @param Parser $parser
449     * @param string $text
450     * @param string ...$forms What to output for each number (singular, dual, plural, etc.)
451     * @return string
452     */
453    public static function plural( $parser, $text = '', ...$forms ) {
454        $text = $parser->getTargetLanguage()->parseFormattedNumber( $text );
455        settype( $text, ctype_digit( $text ) ? 'int' : 'float' );
456        // @phan-suppress-next-line PhanTypeMismatchArgument Phan does not handle settype
457        return $parser->getTargetLanguage()->convertPlural( $text, $forms );
458    }
459
460    public static function formal( Parser $parser, string ...$forms ): string {
461        $index = $parser->getTargetLanguage()->getFormalityIndex();
462        return $forms[$index] ?? $forms[0];
463    }
464
465    /**
466     * @param Parser $parser
467     * @param string $text
468     * @return string
469     */
470    public static function bidi( $parser, $text = '' ) {
471        return $parser->getTargetLanguage()->embedBidi( $text );
472    }
473
474    /**
475     * Override the title of the page when viewed, provided we've been given a
476     * title which will normalise to the canonical title
477     *
478     * @param Parser $parser Parent parser
479     * @param string $text Desired title text
480     * @param string $uarg
481     * @return string
482     */
483    public static function displaytitle( $parser, $text = '', $uarg = '' ) {
484        $restrictDisplayTitle = MediaWikiServices::getInstance()->getMainConfig()
485            ->get( MainConfigNames::RestrictDisplayTitle );
486
487        static $magicWords = null;
488        if ( $magicWords === null ) {
489            $magicWords = $parser->getMagicWordFactory()->newArray(
490                [ 'displaytitle_noerror', 'displaytitle_noreplace' ] );
491        }
492        $arg = $magicWords->matchStartToEnd( $uarg );
493
494        // parse a limited subset of wiki markup (just the single quote items)
495        $text = $parser->doQuotes( $text );
496
497        // remove stripped text (e.g. the UNIQ-QINU stuff) that was generated by tag extensions/whatever
498        $text = $parser->killMarkers( $text );
499
500        // See T28547 for rationale for this processing.
501        // list of disallowed tags for DISPLAYTITLE
502        // these will be escaped even though they are allowed in normal wiki text
503        $bad = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'blockquote', 'ol', 'ul', 'li', 'hr',
504            'table', 'tr', 'th', 'td', 'dl', 'dd', 'caption', 'p', 'ruby', 'rb', 'rt', 'rtc', 'rp', 'br' ];
505
506        // disallow some styles that could be used to bypass $wgRestrictDisplayTitle
507        if ( $restrictDisplayTitle ) {
508            // This code is tested with the cases marked T28547 in
509            // parserTests.txt
510            $htmlTagsCallback = static function ( Attributes $attr ): Attributes {
511                $decoded = $attr->getValues();
512
513                if ( isset( $decoded['style'] ) ) {
514                    // this is called later anyway, but we need it right now for the regexes below to be safe
515                    // calling it twice doesn't hurt
516                    $decoded['style'] = Sanitizer::checkCss( $decoded['style'] );
517
518                    if ( preg_match( '/(display|user-select|visibility)\s*:/i', $decoded['style'] ) ) {
519                        $decoded['style'] = '/* attempt to bypass $wgRestrictDisplayTitle */';
520                    }
521                }
522
523                return new PlainAttributes( $decoded );
524            };
525        } else {
526            $htmlTagsCallback = null;
527        }
528
529        // only requested titles that normalize to the actual title are allowed through
530        // if $wgRestrictDisplayTitle is true (it is by default)
531        // mimic the escaping process that occurs in OutputPage::setPageTitle
532        $text = Sanitizer::removeSomeTags( $text, [
533            'attrCallback' => $htmlTagsCallback,
534            'removeTags' => $bad,
535        ] );
536        $title = Title::newFromText( Sanitizer::stripAllTags( $text ) );
537        // Decode entities in $text the same way that Title::newFromText does
538        $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
539
540        if ( !$restrictDisplayTitle ||
541            ( $title instanceof Title
542            && !$title->hasFragment()
543            && $title->equals( $parser->getTitle() ) )
544        ) {
545            $old = $parser->getOutput()->getPageProperty( 'displaytitle' );
546            if ( $old === null || $arg !== 'displaytitle_noreplace' ) {
547                $parser->getOutput()->setDisplayTitle( $text );
548            }
549            if ( $old !== null && $old !== $text && !$arg ) {
550
551                $converter = $parser->getTargetLanguageConverter();
552                return '<span class="error">' .
553                    $parser->msg( 'duplicate-displaytitle',
554                        // Message should be parsed, but these params should only be escaped.
555                        $converter->markNoConversion( wfEscapeWikiText( $old ) ),
556                        $converter->markNoConversion( wfEscapeWikiText( $filteredText ) )
557                    )->text() .
558                    '</span>';
559            } else {
560                return '';
561            }
562        } else {
563            $parser->getOutput()->addWarningMsg(
564                'restricted-displaytitle',
565                // Message should be parsed, but this param should only be escaped.
566                Message::plaintextParam( $filteredText )
567            );
568            $parser->addTrackingCategory( 'restricted-displaytitle-ignored' );
569        }
570    }
571
572    /**
573     * Matches the given value against the value of given magic word
574     *
575     * @param MagicWordFactory $magicWordFactory A factory to get the word from, e.g., from
576     *   $parser->getMagicWordFactory()
577     * @param string $magicword Magic word key
578     * @param string $value Value to match
579     * @return bool True on successful match
580     */
581    private static function matchAgainstMagicword(
582        MagicWordFactory $magicWordFactory, $magicword, $value
583    ) {
584        $value = trim( strval( $value ) );
585        if ( $value === '' ) {
586            return false;
587        }
588        $mwObject = $magicWordFactory->get( $magicword );
589        return $mwObject->matchStartToEnd( $value );
590    }
591
592    /**
593     * Formats a number according to a language.
594     *
595     * @param int|float $num
596     * @param ?string $raw
597     * @param Language $language
598     * @param MagicWordFactory|null $magicWordFactory To evaluate $raw
599     * @return string
600     */
601    public static function formatRaw(
602        $num, $raw, $language, ?MagicWordFactory $magicWordFactory = null
603    ) {
604        if ( $raw !== null && $raw !== '' ) {
605            if ( !$magicWordFactory ) {
606                $magicWordFactory = MediaWikiServices::getInstance()->getMagicWordFactory();
607            }
608            if ( self::matchAgainstMagicword( $magicWordFactory, 'rawsuffix', $raw ) ) {
609                return (string)$num;
610            }
611        }
612        return $language->formatNum( $num );
613    }
614
615    public static function numberofpages( $parser, $raw = null ) {
616        return self::formatRaw( SiteStats::pages(), $raw, $parser->getTargetLanguage() );
617    }
618
619    public static function numberofusers( $parser, $raw = null ) {
620        return self::formatRaw( SiteStats::users(), $raw, $parser->getTargetLanguage() );
621    }
622
623    public static function numberofactiveusers( $parser, $raw = null ) {
624        return self::formatRaw( SiteStats::activeUsers(), $raw, $parser->getTargetLanguage() );
625    }
626
627    public static function numberofarticles( $parser, $raw = null ) {
628        return self::formatRaw( SiteStats::articles(), $raw, $parser->getTargetLanguage() );
629    }
630
631    public static function numberoffiles( $parser, $raw = null ) {
632        return self::formatRaw( SiteStats::images(), $raw, $parser->getTargetLanguage() );
633    }
634
635    public static function numberofadmins( $parser, $raw = null ) {
636        return self::formatRaw(
637            SiteStats::numberingroup( 'sysop' ),
638            $raw,
639            $parser->getTargetLanguage()
640        );
641    }
642
643    public static function numberofedits( $parser, $raw = null ) {
644        return self::formatRaw( SiteStats::edits(), $raw, $parser->getTargetLanguage() );
645    }
646
647    public static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) {
648        return self::formatRaw(
649            SiteStats::pagesInNs( intval( $namespace ) ),
650            $raw,
651            $parser->getTargetLanguage()
652        );
653    }
654
655    public static function numberingroup( $parser, $name = '', $raw = null ) {
656        return self::formatRaw(
657            SiteStats::numberingroup( strtolower( $name ) ),
658            $raw,
659            $parser->getTargetLanguage()
660        );
661    }
662
663    /**
664     * Helper function for preprocessing an optional argument which represents
665     * a title.
666     * @param Parser $parser
667     * @param string|null $t if null, returns the Parser's Title
668     *   for consistency with magic variable forms
669     * @return ?Title
670     */
671    private static function makeTitle( Parser $parser, ?string $t ) {
672        if ( $t === null ) {
673            // For consistency with magic variable forms
674            $title = $parser->getTitle();
675        } else {
676            $title = Title::newFromText( $t );
677        }
678        return $title;
679    }
680
681    /**
682     * Given a title, return the namespace name that would be given by the
683     * corresponding magic word
684     * @param Parser $parser
685     * @param string|null $title
686     * @return mixed|string
687     * @since 1.39
688     */
689    public static function namespace( $parser, $title = null ) {
690        $t = self::makeTitle( $parser, $title );
691        if ( $t === null ) {
692            return '';
693        }
694        return str_replace( '_', ' ', $t->getNsText() );
695    }
696
697    public static function namespacee( $parser, $title = null ) {
698        $t = self::makeTitle( $parser, $title );
699        if ( $t === null ) {
700            return '';
701        }
702        return wfUrlencode( $t->getNsText() );
703    }
704
705    public static function namespacenumber( $parser, $title = null ) {
706        $t = self::makeTitle( $parser, $title );
707        if ( $t === null ) {
708            return '';
709        }
710        return (string)$t->getNamespace();
711    }
712
713    public static function talkspace( $parser, $title = null ) {
714        $t = self::makeTitle( $parser, $title );
715        if ( $t === null || !$t->canHaveTalkPage() ) {
716            return '';
717        }
718        return str_replace( '_', ' ', $t->getTalkNsText() );
719    }
720
721    public static function talkspacee( $parser, $title = null ) {
722        $t = self::makeTitle( $parser, $title );
723        if ( $t === null || !$t->canHaveTalkPage() ) {
724            return '';
725        }
726        return wfUrlencode( $t->getTalkNsText() );
727    }
728
729    public static function subjectspace( $parser, $title = null ) {
730        $t = self::makeTitle( $parser, $title );
731        if ( $t === null ) {
732            return '';
733        }
734        return str_replace( '_', ' ', $t->getSubjectNsText() );
735    }
736
737    public static function subjectspacee( $parser, $title = null ) {
738        $t = self::makeTitle( $parser, $title );
739        if ( $t === null ) {
740            return '';
741        }
742        return wfUrlencode( $t->getSubjectNsText() );
743    }
744
745    /**
746     * Functions to get and normalize pagenames, corresponding to the magic words
747     * of the same names
748     * @param Parser $parser
749     * @param string|null $title
750     * @return string
751     */
752    public static function pagename( $parser, $title = null ) {
753        $t = self::makeTitle( $parser, $title );
754        if ( $t === null ) {
755            return '';
756        }
757        return wfEscapeWikiText( $t->getText() );
758    }
759
760    public static function pagenamee( $parser, $title = null ) {
761        $t = self::makeTitle( $parser, $title );
762        if ( $t === null ) {
763            return '';
764        }
765        return wfEscapeWikiText( $t->getPartialURL() );
766    }
767
768    public static function fullpagename( $parser, $title = null ) {
769        $t = self::makeTitle( $parser, $title );
770        if ( $t === null ) {
771            return '';
772        }
773        return wfEscapeWikiText( $t->getPrefixedText() );
774    }
775
776    public static function fullpagenamee( $parser, $title = null ) {
777        $t = self::makeTitle( $parser, $title );
778        if ( $t === null ) {
779            return '';
780        }
781        return wfEscapeWikiText( $t->getPrefixedURL() );
782    }
783
784    public static function subpagename( $parser, $title = null ) {
785        $t = self::makeTitle( $parser, $title );
786        if ( $t === null ) {
787            return '';
788        }
789        return wfEscapeWikiText( $t->getSubpageText() );
790    }
791
792    public static function subpagenamee( $parser, $title = null ) {
793        $t = self::makeTitle( $parser, $title );
794        if ( $t === null ) {
795            return '';
796        }
797        return wfEscapeWikiText( $t->getSubpageUrlForm() );
798    }
799
800    public static function rootpagename( $parser, $title = null ) {
801        $t = self::makeTitle( $parser, $title );
802        if ( $t === null ) {
803            return '';
804        }
805        return wfEscapeWikiText( $t->getRootText() );
806    }
807
808    public static function rootpagenamee( $parser, $title = null ) {
809        $t = self::makeTitle( $parser, $title );
810        if ( $t === null ) {
811            return '';
812        }
813        return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getRootText() ) ) );
814    }
815
816    public static function basepagename( $parser, $title = null ) {
817        $t = self::makeTitle( $parser, $title );
818        if ( $t === null ) {
819            return '';
820        }
821        return wfEscapeWikiText( $t->getBaseText() );
822    }
823
824    public static function basepagenamee( $parser, $title = null ) {
825        $t = self::makeTitle( $parser, $title );
826        if ( $t === null ) {
827            return '';
828        }
829        return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getBaseText() ) ) );
830    }
831
832    public static function talkpagename( $parser, $title = null ) {
833        $t = self::makeTitle( $parser, $title );
834        if ( $t === null || !$t->canHaveTalkPage() ) {
835            return '';
836        }
837        return wfEscapeWikiText( $t->getTalkPage()->getPrefixedText() );
838    }
839
840    public static function talkpagenamee( $parser, $title = null ) {
841        $t = self::makeTitle( $parser, $title );
842        if ( $t === null || !$t->canHaveTalkPage() ) {
843            return '';
844        }
845        return wfEscapeWikiText( $t->getTalkPage()->getPrefixedURL() );
846    }
847
848    public static function subjectpagename( $parser, $title = null ) {
849        $t = self::makeTitle( $parser, $title );
850        if ( $t === null ) {
851            return '';
852        }
853        return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedText() );
854    }
855
856    public static function subjectpagenamee( $parser, $title = null ) {
857        $t = self::makeTitle( $parser, $title );
858        if ( $t === null ) {
859            return '';
860        }
861        return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedURL() );
862    }
863
864    /**
865     * Return the number of pages, files or subcats in the given category,
866     * or 0 if it's nonexistent. This is an expensive parser function and
867     * can't be called too many times per page.
868     * @param Parser $parser
869     * @param string $name
870     * @param string $arg1
871     * @param string $arg2
872     * @return string
873     */
874    public static function pagesincategory( $parser, $name = '', $arg1 = '', $arg2 = '' ) {
875        static $magicWords = null;
876        if ( $magicWords === null ) {
877            $magicWords = $parser->getMagicWordFactory()->newArray( [
878                'pagesincategory_all',
879                'pagesincategory_pages',
880                'pagesincategory_subcats',
881                'pagesincategory_files'
882            ] );
883        }
884        static $cache = [];
885
886        // split the given option to its variable
887        if ( self::matchAgainstMagicword( $parser->getMagicWordFactory(), 'rawsuffix', $arg1 ) ) {
888            // {{pagesincategory:|raw[|type]}}
889            $raw = $arg1;
890            $type = $magicWords->matchStartToEnd( $arg2 );
891        } else {
892            // {{pagesincategory:[|type[|raw]]}}
893            $type = $magicWords->matchStartToEnd( $arg1 );
894            $raw = $arg2;
895        }
896        if ( !$type ) { // backward compatibility
897            $type = 'pagesincategory_all';
898        }
899
900        $title = Title::makeTitleSafe( NS_CATEGORY, $name );
901        if ( !$title ) { # invalid title
902            return self::formatRaw( 0, $raw, $parser->getTargetLanguage() );
903        }
904        $languageConverter = MediaWikiServices::getInstance()
905            ->getLanguageConverterFactory()
906            ->getLanguageConverter( $parser->getContentLanguage() );
907        $languageConverter->findVariantLink( $name, $title, true );
908
909        // Normalize name for cache
910        $name = $title->getDBkey();
911
912        if ( !isset( $cache[$name] ) ) {
913            $category = Category::newFromTitle( $title );
914
915            $allCount = $subcatCount = $fileCount = $pageCount = 0;
916            if ( $parser->incrementExpensiveFunctionCount() ) {
917                $allCount = $category->getMemberCount();
918                $subcatCount = $category->getSubcatCount();
919                $fileCount = $category->getFileCount();
920                $pageCount = $category->getPageCount( Category::COUNT_CONTENT_PAGES );
921            }
922            $cache[$name]['pagesincategory_all'] = $allCount;
923            $cache[$name]['pagesincategory_pages'] = $pageCount;
924            $cache[$name]['pagesincategory_subcats'] = $subcatCount;
925            $cache[$name]['pagesincategory_files'] = $fileCount;
926        }
927
928        $count = $cache[$name][$type];
929        return self::formatRaw( $count, $raw, $parser->getTargetLanguage() );
930    }
931
932    /**
933     * Return the size of the given page, or 0 if it's nonexistent.  This is an
934     * expensive parser function and can't be called too many times per page.
935     *
936     * @param Parser $parser
937     * @param string $page Name of page to check (Default: empty string)
938     * @param string|null $raw Should number be human readable with commas or just number
939     * @return string
940     */
941    public static function pagesize( $parser, $page = '', $raw = null ) {
942        $title = Title::newFromText( $page );
943
944        if ( !is_object( $title ) || $title->isExternal() ) {
945            return self::formatRaw( 0, $raw, $parser->getTargetLanguage() );
946        }
947
948        // fetch revision from cache/database and return the value
949        $rev = self::getCachedRevisionObject( $parser, $title, ParserOutputFlags::VARY_REVISION_SHA1 );
950        $length = $rev ? $rev->getSize() : 0;
951        if ( $length === null ) {
952            // We've had bugs where rev_len was not being recorded for empty pages, see T135414
953            $length = 0;
954        }
955        return self::formatRaw( $length, $raw, $parser->getTargetLanguage() );
956    }
957
958    /**
959     * Returns the requested protection level for the current page. This
960     * is an expensive parser function and can't be called too many times
961     * per page, unless the protection levels/expiries for the given title
962     * have already been retrieved
963     *
964     * @param Parser $parser
965     * @param string $type
966     * @param string $title
967     *
968     * @return string
969     */
970    public static function protectionlevel( $parser, $type = '', $title = '' ) {
971        $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
972        $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
973        if ( $restrictionStore->areRestrictionsLoaded( $titleObject ) || $parser->incrementExpensiveFunctionCount() ) {
974            $restrictions = $restrictionStore->getRestrictions( $titleObject, strtolower( $type ) );
975            # RestrictionStore::getRestrictions returns an array, its possible it may have
976            # multiple values in the future
977            return implode( ',', $restrictions );
978        }
979        return '';
980    }
981
982    /**
983     * Returns the requested protection expiry for the current page. This
984     * is an expensive parser function and can't be called too many times
985     * per page, unless the protection levels/expiries for the given title
986     * have already been retrieved
987     *
988     * @param Parser $parser
989     * @param string $type
990     * @param string $title
991     *
992     * @return string
993     */
994    public static function protectionexpiry( $parser, $type = '', $title = '' ) {
995        $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
996        $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
997        if ( $restrictionStore->areRestrictionsLoaded( $titleObject ) || $parser->incrementExpensiveFunctionCount() ) {
998            // getRestrictionExpiry() returns null on invalid type; trying to
999            // match protectionlevel() function that returns empty string instead
1000            return $restrictionStore->getRestrictionExpiry( $titleObject, strtolower( $type ) ) ?? '';
1001        }
1002        return '';
1003    }
1004
1005    /**
1006     * Gives language names.
1007     * @param Parser $parser
1008     * @param string $code Language code (of which to get name)
1009     * @param string $inLanguage Language code (in which to get name);
1010     *   if missing or empty, the language's autonym will be returned.
1011     * @return string
1012     */
1013    public static function language( $parser, $code = '', $inLanguage = '' ) {
1014        if ( $code === '' ) {
1015            $code = $parser->getTargetLanguage()->getCode();
1016        }
1017        if ( $inLanguage === '' ) {
1018            $inLanguage = LanguageNameUtils::AUTONYMS;
1019        }
1020        $lang = MediaWikiServices::getInstance()
1021            ->getLanguageNameUtils()
1022            ->getLanguageName( $code, $inLanguage );
1023        return $lang !== '' ? $lang : LanguageCode::bcp47( $code );
1024    }
1025
1026    /**
1027     * Gives direction of script of a language given a language code.
1028     * @param Parser $parser
1029     * @param string $code a language code. If missing, the parser target
1030     *  language will be used.
1031     * @param string $arg If this optional argument matches the
1032     *  `language_option_bcp47` magic word, the language code will be treated
1033     *  as a BCP-47 code.
1034     * @return string 'rtl' if the language code is recognized as
1035     *  right-to-left, otherwise returns 'ltr'
1036     */
1037    public static function dir( Parser $parser, string $code = '', string $arg = '' ): string {
1038        static $magicWords = null;
1039        $languageFactory = MediaWikiServices::getInstance()->getLanguageFactory();
1040
1041        if ( $code === '' ) {
1042            $lang = $parser->getTargetLanguage();
1043        } else {
1044            if ( $arg !== '' ) {
1045                if ( $magicWords === null ) {
1046                    $magicWords = $parser->getMagicWordFactory()->newArray( [ 'language_option_bcp47' ] );
1047                }
1048                if ( $magicWords->matchStartToEnd( $arg ) === 'language_option_bcp47' ) {
1049                    // Prefer the BCP-47 interpretation of this code.
1050                    $code = new Bcp47CodeValue( $code );
1051                }
1052            }
1053            try {
1054                $lang = $languageFactory->getLanguage( $code );
1055            } catch ( InvalidArgumentException $ex ) {
1056                $parser->addTrackingCategory( 'bad-language-code-category' );
1057                return 'ltr';
1058            }
1059        }
1060        return $lang->getDir();
1061    }
1062
1063    /**
1064     * Gives the BCP-47 code for a language given the mediawiki internal
1065     * language code.
1066     * @param Parser $parser
1067     * @param string $code a language code. If missing, the parser target
1068     *  language will be used.
1069     * @return string the corresponding BCP-47 code
1070     */
1071    public static function bcp47( Parser $parser, string $code = '' ): string {
1072        if ( $code === '' ) {
1073            return $parser->getTargetLanguage()->toBcp47Code();
1074        } else {
1075            return LanguageCode::bcp47( $code );
1076        }
1077    }
1078
1079    /**
1080     * Unicode-safe str_pad with the restriction that $length is forced to be <= 500
1081     * @param Parser $parser
1082     * @param string $string
1083     * @param string $length
1084     * @param string $padding
1085     * @param int $direction
1086     * @return string
1087     */
1088    public static function pad(
1089        $parser, $string, $length, $padding = '0', $direction = STR_PAD_RIGHT
1090    ) {
1091        $padding = $parser->killMarkers( $padding );
1092        $lengthOfPadding = mb_strlen( $padding );
1093        if ( $lengthOfPadding == 0 ) {
1094            return $string;
1095        }
1096
1097        # The remaining length to add counts down to 0 as padding is added
1098        $length = min( (int)$length, 500 ) - mb_strlen( $string );
1099        if ( $length <= 0 ) {
1100            // Nothing to add
1101            return $string;
1102        }
1103
1104        # $finalPadding is just $padding repeated enough times so that
1105        # mb_strlen( $string ) + mb_strlen( $finalPadding ) == $length
1106        $finalPadding = '';
1107        while ( $length > 0 ) {
1108            # If $length < $lengthofPadding, truncate $padding so we get the
1109            # exact length desired.
1110            $finalPadding .= mb_substr( $padding, 0, $length );
1111            $length -= $lengthOfPadding;
1112        }
1113
1114        if ( $direction == STR_PAD_LEFT ) {
1115            return $finalPadding . $string;
1116        } else {
1117            return $string . $finalPadding;
1118        }
1119    }
1120
1121    public static function padleft( $parser, $string = '', $length = 0, $padding = '0' ) {
1122        return self::pad( $parser, $string, $length, $padding, STR_PAD_LEFT );
1123    }
1124
1125    public static function padright( $parser, $string = '', $length = 0, $padding = '0' ) {
1126        return self::pad( $parser, $string, $length, $padding );
1127    }
1128
1129    /**
1130     * @param Parser $parser
1131     * @param string $text
1132     * @return string
1133     */
1134    public static function anchorencode( $parser, $text ) {
1135        $text = $parser->killMarkers( $text );
1136        $section = (string)substr( $parser->guessSectionNameFromWikiText( $text ), 1 );
1137        return Sanitizer::safeEncodeAttribute( $section );
1138    }
1139
1140    public static function special( $parser, $text ) {
1141        [ $page, $subpage ] = MediaWikiServices::getInstance()->getSpecialPageFactory()->
1142            resolveAlias( $text );
1143        if ( $page ) {
1144            $title = SpecialPage::getTitleFor( $page, $subpage );
1145            return $title->getPrefixedText();
1146        } else {
1147            // unknown special page, just use the given text as its title, if at all possible
1148            $title = Title::makeTitleSafe( NS_SPECIAL, $text );
1149            return $title ? $title->getPrefixedText() : self::special( $parser, 'Badtitle' );
1150        }
1151    }
1152
1153    public static function speciale( $parser, $text ) {
1154        return wfUrlencode( str_replace( ' ', '_', self::special( $parser, $text ) ) );
1155    }
1156
1157    /**
1158     * @param Parser $parser
1159     * @param string $text The sortkey to use
1160     * @param string $uarg Either "noreplace" or "noerror" (in en)
1161     *   both suppress errors, and noreplace does nothing if
1162     *   a default sortkey already exists.
1163     * @return string
1164     */
1165    public static function defaultsort( $parser, $text, $uarg = '' ) {
1166        static $magicWords = null;
1167        if ( $magicWords === null ) {
1168            $magicWords = $parser->getMagicWordFactory()->newArray(
1169                [ 'defaultsort_noerror', 'defaultsort_noreplace' ] );
1170        }
1171        $arg = $magicWords->matchStartToEnd( $uarg );
1172
1173        $text = trim( $text );
1174        if ( strlen( $text ) == 0 ) {
1175            return '';
1176        }
1177        $old = $parser->getOutput()->getPageProperty( 'defaultsort' );
1178        if ( $old === null || $arg !== 'defaultsort_noreplace' ) {
1179            $parser->getOutput()->setPageProperty( 'defaultsort', $text );
1180        }
1181
1182        if ( $old === null || $old == $text || $arg ) {
1183            return '';
1184        } else {
1185            $converter = $parser->getTargetLanguageConverter();
1186            return '<span class="error">' .
1187                $parser->msg( 'duplicate-defaultsort',
1188                    // Message should be parsed, but these params should only be escaped.
1189                    $converter->markNoConversion( wfEscapeWikiText( $old ) ),
1190                    $converter->markNoConversion( wfEscapeWikiText( $text ) )
1191                )->text() .
1192                '</span>';
1193        }
1194    }
1195
1196    /**
1197     * Usage {{filepath|300}}, {{filepath|nowiki}}, {{filepath|nowiki|300}}
1198     * or {{filepath|300|nowiki}} or {{filepath|300px}}, {{filepath|200x300px}},
1199     * {{filepath|nowiki|200x300px}}, {{filepath|200x300px|nowiki}}.
1200     *
1201     * @param Parser $parser
1202     * @param string $name
1203     * @param string $argA
1204     * @param string $argB
1205     * @return array|string
1206     */
1207    public static function filepath( $parser, $name = '', $argA = '', $argB = '' ) {
1208        $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $name );
1209
1210        if ( $argA == 'nowiki' ) {
1211            // {{filepath: | option [| size] }}
1212            $isNowiki = true;
1213            $parsedWidthParam = $parser->parseWidthParam( $argB );
1214        } else {
1215            // {{filepath: [| size [|option]] }}
1216            $parsedWidthParam = $parser->parseWidthParam( $argA );
1217            $isNowiki = ( $argB == 'nowiki' );
1218        }
1219
1220        if ( $file ) {
1221            $url = $file->getFullUrl();
1222
1223            // If a size is requested...
1224            if ( count( $parsedWidthParam ) ) {
1225                $mto = $file->transform( $parsedWidthParam );
1226                // ... and we can
1227                if ( $mto && !$mto->isError() ) {
1228                    // ... change the URL to point to a thumbnail.
1229                    $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
1230                    $url = $urlUtils->expand( $mto->getUrl(), PROTO_RELATIVE ) ?? false;
1231                }
1232            }
1233            if ( $isNowiki ) {
1234                return [ $url, 'nowiki' => true ];
1235            }
1236            return $url;
1237        } else {
1238            return '';
1239        }
1240    }
1241
1242    /**
1243     * Parser function to extension tag adaptor
1244     * @param Parser $parser
1245     * @param PPFrame $frame
1246     * @param PPNode[] $args
1247     * @return string
1248     */
1249    public static function tagObj( $parser, $frame, $args ) {
1250        if ( !count( $args ) ) {
1251            return '';
1252        }
1253        $tagName = strtolower( trim( $frame->expand( array_shift( $args ) ) ) );
1254        $processNowiki = $parser->tagNeedsNowikiStrippedInTagPF( $tagName ) ? PPFrame::PROCESS_NOWIKI : 0;
1255
1256        if ( count( $args ) ) {
1257            $inner = $frame->expand( array_shift( $args ), $processNowiki );
1258        } else {
1259            $inner = null;
1260        }
1261
1262        $attributes = [];
1263        foreach ( $args as $arg ) {
1264            $bits = $arg->splitArg();
1265            if ( strval( $bits['index'] ) === '' ) {
1266                $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
1267                $value = trim( $frame->expand( $bits['value'] ) );
1268                if ( preg_match( '/^(?:["\'](.+)["\']|""|\'\')$/s', $value, $m ) ) {
1269                    $value = $m[1] ?? '';
1270                }
1271                $attributes[$name] = $value;
1272            }
1273        }
1274
1275        $stripList = $parser->getStripList();
1276        if ( !in_array( $tagName, $stripList ) ) {
1277            // we can't handle this tag (at least not now), so just re-emit it as an ordinary tag
1278            $attrText = '';
1279            foreach ( $attributes as $name => $value ) {
1280                $attrText .= ' ' . htmlspecialchars( $name ) .
1281                    '="' . htmlspecialchars( $value, ENT_COMPAT ) . '"';
1282            }
1283            if ( $inner === null ) {
1284                return "<$tagName$attrText/>";
1285            }
1286            return "<$tagName$attrText>$inner</$tagName>";
1287        }
1288
1289        $params = [
1290            'name' => $tagName,
1291            'inner' => $inner,
1292            'attributes' => $attributes,
1293            'close' => "</$tagName>",
1294        ];
1295        return $parser->extensionSubstitution( $params, $frame );
1296    }
1297
1298    /**
1299     * Fetched the current revision of the given title and return this.
1300     * Will increment the expensive function count and
1301     * add a template link to get the value refreshed on changes.
1302     * For a given title, which is equal to the current parser title,
1303     * the RevisionRecord object from the parser is used, when that is the current one
1304     *
1305     * @param Parser $parser
1306     * @param Title $title
1307     * @param string $vary ParserOutput vary-* flag
1308     * @return RevisionRecord|null
1309     * @since 1.23
1310     */
1311    private static function getCachedRevisionObject( $parser, $title, $vary ) {
1312        if ( !$title ) {
1313            return null;
1314        }
1315
1316        $revisionRecord = null;
1317
1318        $isSelfReferential = $title->equals( $parser->getTitle() );
1319        if ( $isSelfReferential ) {
1320            // Revision is for the same title that is currently being parsed. Only use the last
1321            // saved revision, regardless of Parser::getRevisionId() or fake revision injection
1322            // callbacks against the current title.
1323
1324            // FIXME (T318278): the above is the intention, but doesn't
1325            // describe the actual current behavior of this code, since
1326            // ->isCurrent() for the last saved revision will return
1327            // false so we're going to fall through and end up calling
1328            // ->getCurrentRevisionRecordOfTitle().
1329            $parserRevisionRecord = $parser->getRevisionRecordObject();
1330            if ( $parserRevisionRecord && $parserRevisionRecord->isCurrent() ) {
1331                $revisionRecord = $parserRevisionRecord;
1332            }
1333        }
1334
1335        $parserOutput = $parser->getOutput();
1336        if ( !$revisionRecord ) {
1337            if (
1338                !$parser->isCurrentRevisionOfTitleCached( $title ) &&
1339                !$parser->incrementExpensiveFunctionCount()
1340            ) {
1341                return null; // not allowed
1342            }
1343            // Get the current revision, ignoring Parser::getRevisionId() being null/old
1344            $revisionRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
1345            if ( !$revisionRecord ) {
1346                // Convert `false` error return to `null`
1347                $revisionRecord = null;
1348            }
1349            // Register dependency in templatelinks
1350            $parserOutput->addTemplate(
1351                $title,
1352                $revisionRecord ? $revisionRecord->getPageId() : 0,
1353                $revisionRecord ? $revisionRecord->getId() : 0
1354            );
1355        }
1356
1357        if ( $isSelfReferential ) {
1358            wfDebug( __METHOD__ . ": used current revision, setting $vary" );
1359            // Upon page save, the result of the parser function using this might change
1360            $parserOutput->setOutputFlag( $vary );
1361            if ( $vary === ParserOutputFlags::VARY_REVISION_SHA1 && $revisionRecord ) {
1362                try {
1363                    $sha1 = $revisionRecord->getSha1();
1364                } catch ( RevisionAccessException $e ) {
1365                    $sha1 = null;
1366                }
1367                $parserOutput->setRevisionUsedSha1Base36( $sha1 );
1368            }
1369        }
1370
1371        return $revisionRecord;
1372    }
1373
1374    /**
1375     * Get the pageid of a specified page
1376     * @param Parser $parser
1377     * @param string|null $title Title to get the pageid from
1378     * @return int|null|string
1379     * @since 1.23
1380     */
1381    public static function pageid( $parser, $title = null ) {
1382        $t = self::makeTitle( $parser, $title );
1383        if ( !$t ) {
1384            return '';
1385        } elseif ( !$t->canExist() || $t->isExternal() ) {
1386            return 0; // e.g. special page or interwiki link
1387        }
1388
1389        $parserOutput = $parser->getOutput();
1390
1391        if ( $t->equals( $parser->getTitle() ) ) {
1392            // Revision is for the same title that is currently being parsed.
1393            // Use the title from Parser in case a new page ID was injected into it.
1394            $parserOutput->setOutputFlag( ParserOutputFlags::VARY_PAGE_ID );
1395            $id = $parser->getTitle()->getArticleID();
1396            if ( $id ) {
1397                $parserOutput->setSpeculativePageIdUsed( $id );
1398            }
1399
1400            return $id;
1401        }
1402
1403        // Check the link cache for the title
1404        $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1405        $pdbk = $t->getPrefixedDBkey();
1406        $id = $linkCache->getGoodLinkID( $pdbk );
1407        if ( $id != 0 || $linkCache->isBadLink( $pdbk ) ) {
1408            $parserOutput->addLink( $t, $id );
1409
1410            return $id;
1411        }
1412
1413        // We need to load it from the DB, so mark expensive
1414        if ( $parser->incrementExpensiveFunctionCount() ) {
1415            $id = $t->getArticleID();
1416            $parserOutput->addLink( $t, $id );
1417
1418            return $id;
1419        }
1420
1421        return null;
1422    }
1423
1424    /**
1425     * Get the id from the last revision of a specified page.
1426     * @param Parser $parser
1427     * @param string|null $title Title to get the id from
1428     * @return int|null|string
1429     * @since 1.23
1430     */
1431    public static function revisionid( $parser, $title = null ) {
1432        $t = self::makeTitle( $parser, $title );
1433        if ( $t === null || $t->isExternal() ) {
1434            return '';
1435        }
1436
1437        $services = MediaWikiServices::getInstance();
1438        if (
1439            $t->equals( $parser->getTitle() ) &&
1440            $services->getMainConfig()->get( MainConfigNames::MiserMode ) &&
1441            !$parser->getOptions()->getInterfaceMessage() &&
1442            // @TODO: disallow this word on all namespaces (T235957)
1443            $services->getNamespaceInfo()->isSubject( $t->getNamespace() )
1444        ) {
1445            // Use a stub result instead of the actual revision ID in order to avoid
1446            // double parses on page save but still allow preview detection (T137900)
1447            if ( $parser->getRevisionId() || $parser->getOptions()->getSpeculativeRevId() ) {
1448                return '-';
1449            } else {
1450                $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_EXISTS );
1451                return '';
1452            }
1453        }
1454        // Fetch revision from cache/database and return the value.
1455        // Inform the edit saving system that getting the canonical output
1456        // after revision insertion requires a parse that used that exact
1457        // revision ID.
1458        if ( $t->equals( $parser->getTitle() ) && $title === null ) {
1459            // special handling for no-arg case: use speculative rev id
1460            // for current page.
1461            $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_ID );
1462            $id = $parser->getRevisionId();
1463            if ( $id === 0 ) {
1464                $rev = $parser->getRevisionRecordObject();
1465                if ( $rev ) {
1466                    $id = $rev->getId();
1467                }
1468            }
1469            if ( !$id ) {
1470                $id = $parser->getOptions()->getSpeculativeRevId();
1471                if ( $id ) {
1472                    $parser->getOutput()->setSpeculativeRevIdUsed( $id );
1473                }
1474            }
1475            return (string)$id;
1476        }
1477        $rev = self::getCachedRevisionObject( $parser, $t, ParserOutputFlags::VARY_REVISION_ID );
1478        return $rev ? $rev->getId() : '';
1479    }
1480
1481    private static function getRevisionTimestampSubstring(
1482        Parser $parser,
1483        Title $title,
1484        int $start,
1485        int $len,
1486        int $mtts
1487    ): string {
1488        // If fetching the revision timestamp of the current page, substitute the
1489        // speculative timestamp to be used when this revision is saved.  This
1490        // avoids having to invalidate the cache immediately by assuming the "last
1491        // saved revision" will in fact be this one.
1492        // Don't do this for interface messages (eg, edit notices) however; in that
1493        // case fall through and use the actual timestamp of the last saved revision.
1494        if ( $title->equals( $parser->getTitle() ) && !$parser->getOptions()->getInterfaceMessage() ) {
1495            // Get the timezone-adjusted timestamp to be used for this revision
1496            $resNow = substr( $parser->getRevisionTimestamp(), $start, $len );
1497            // Possibly set vary-revision if there is not yet an associated revision
1498            if ( !$parser->getRevisionRecordObject() ) {
1499                // Get the timezone-adjusted timestamp $mtts seconds in the future.
1500                // This future is relative to the current time and not that of the
1501                // parser options. The rendered timestamp can be compared to that
1502                // of the timestamp specified by the parser options.
1503                $resThen = substr(
1504                    $parser->getContentLanguage()->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
1505                    $start,
1506                    $len
1507                );
1508
1509                if ( $resNow !== $resThen ) {
1510                    // Inform the edit saving system that getting the canonical output after
1511                    // revision insertion requires a parse that used an actual revision timestamp
1512                    $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_TIMESTAMP );
1513                }
1514            }
1515
1516            return $resNow;
1517        } else {
1518            $rev = self::getCachedRevisionObject( $parser, $title, ParserOutputFlags::VARY_REVISION_TIMESTAMP );
1519            if ( !$rev ) {
1520                return '';
1521            }
1522            $resNow = substr(
1523                $parser->getContentLanguage()->userAdjust( $rev->getTimestamp(), '' ), $start, $len
1524            );
1525            return $resNow;
1526        }
1527    }
1528
1529    /**
1530     * Get the day from the last revision of a specified page.
1531     * @param Parser $parser
1532     * @param string|null $title Title to get the day from
1533     * @return string
1534     * @since 1.23
1535     */
1536    public static function revisionday( $parser, $title = null ) {
1537        $t = self::makeTitle( $parser, $title );
1538        if ( $t === null || $t->isExternal() ) {
1539            return '';
1540        }
1541        return strval( (int)self::getRevisionTimestampSubstring(
1542            $parser, $t, 6, 2, self::MAX_TTS
1543        ) );
1544    }
1545
1546    /**
1547     * Get the day with leading zeros from the last revision of a specified page.
1548     * @param Parser $parser
1549     * @param string|null $title Title to get the day from
1550     * @return string
1551     * @since 1.23
1552     */
1553    public static function revisionday2( $parser, $title = null ) {
1554        $t = self::makeTitle( $parser, $title );
1555        if ( $t === null || $t->isExternal() ) {
1556            return '';
1557        }
1558        return self::getRevisionTimestampSubstring(
1559            $parser, $t, 6, 2, self::MAX_TTS
1560        );
1561    }
1562
1563    /**
1564     * Get the month with leading zeros from the last revision of a specified page.
1565     * @param Parser $parser
1566     * @param string|null $title Title to get the month from
1567     * @return string
1568     * @since 1.23
1569     */
1570    public static function revisionmonth( $parser, $title = null ) {
1571        $t = self::makeTitle( $parser, $title );
1572        if ( $t === null || $t->isExternal() ) {
1573            return '';
1574        }
1575        return self::getRevisionTimestampSubstring(
1576            $parser, $t, 4, 2, self::MAX_TTS
1577        );
1578    }
1579
1580    /**
1581     * Get the month from the last revision of a specified page.
1582     * @param Parser $parser
1583     * @param string|null $title Title to get the month from
1584     * @return string
1585     * @since 1.23
1586     */
1587    public static function revisionmonth1( $parser, $title = null ) {
1588        $t = self::makeTitle( $parser, $title );
1589        if ( $t === null || $t->isExternal() ) {
1590            return '';
1591        }
1592        return strval( (int)self::getRevisionTimestampSubstring(
1593            $parser, $t, 4, 2, self::MAX_TTS
1594        ) );
1595    }
1596
1597    /**
1598     * Get the year from the last revision of a specified page.
1599     * @param Parser $parser
1600     * @param string|null $title Title to get the year from
1601     * @return string
1602     * @since 1.23
1603     */
1604    public static function revisionyear( $parser, $title = null ) {
1605        $t = self::makeTitle( $parser, $title );
1606        if ( $t === null || $t->isExternal() ) {
1607            return '';
1608        }
1609        return self::getRevisionTimestampSubstring(
1610            $parser, $t, 0, 4, self::MAX_TTS
1611        );
1612    }
1613
1614    /**
1615     * Get the timestamp from the last revision of a specified page.
1616     * @param Parser $parser
1617     * @param string|null $title Title to get the timestamp from
1618     * @return string
1619     * @since 1.23
1620     */
1621    public static function revisiontimestamp( $parser, $title = null ) {
1622        $t = self::makeTitle( $parser, $title );
1623        if ( $t === null || $t->isExternal() ) {
1624            return '';
1625        }
1626        return self::getRevisionTimestampSubstring(
1627            $parser, $t, 0, 14, self::MAX_TTS
1628        );
1629    }
1630
1631    /**
1632     * Get the user from the last revision of a specified page.
1633     * @param Parser $parser
1634     * @param string|null $title Title to get the user from
1635     * @return string
1636     * @since 1.23
1637     */
1638    public static function revisionuser( $parser, $title = null ) {
1639        $t = self::makeTitle( $parser, $title );
1640        if ( $t === null || $t->isExternal() ) {
1641            return '';
1642        }
1643        // VARY_USER informs the edit saving system that getting the canonical
1644        // output after revision insertion requires a parse that used the
1645        // actual user ID.
1646        if ( $t->equals( $parser->getTitle() ) ) {
1647            // Fall back to Parser's "revision user" for the current title
1648            $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_USER );
1649            // Note that getRevisionUser() can return null; we need to
1650            // be sure to cast this to (an empty) string, since returning
1651            // null means "magic variable not handled".
1652            return (string)$parser->getRevisionUser();
1653        }
1654        // Fetch revision from cache/database and return the value.
1655        $rev = self::getCachedRevisionObject( $parser, $t, ParserOutputFlags::VARY_USER );
1656        $user = ( $rev !== null ) ? $rev->getUser() : null;
1657        return $user ? $user->getName() : '';
1658    }
1659
1660    /**
1661     * Returns the sources of any cascading protection acting on a specified page.
1662     * Pages will not return their own title unless they transclude themselves.
1663     * This is an expensive parser function and can't be called too many times per page,
1664     * unless cascading protection sources for the page have already been loaded.
1665     *
1666     * @param Parser $parser
1667     * @param ?string $title
1668     *
1669     * @return string
1670     * @since 1.23
1671     */
1672    public static function cascadingsources( $parser, $title = '' ) {
1673        $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
1674        $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
1675        if ( $restrictionStore->areCascadeProtectionSourcesLoaded( $titleObject )
1676            || $parser->incrementExpensiveFunctionCount()
1677        ) {
1678            $names = [];
1679            $sources = $restrictionStore->getCascadeProtectionSources( $titleObject );
1680            $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
1681            foreach ( $sources[0] as $sourcePageIdentity ) {
1682                $names[] = $titleFormatter->getPrefixedText( $sourcePageIdentity );
1683            }
1684            return implode( '|', $names );
1685        }
1686        return '';
1687    }
1688
1689    public static function interwikilink( $parser, $prefix = '', $title = '', $linkText = null ) {
1690        $services = MediaWikiServices::getInstance();
1691        if (
1692            $prefix !== '' &&
1693            $services->getInterwikiLookup()->isValidInterwiki( $prefix )
1694        ) {
1695            if ( $linkText !== null ) {
1696                $linkText = Parser::stripOuterParagraph(
1697                    # FIXME T382287: when using Parsoid this may leave
1698                    # strip markers behind for embedded extension tags.
1699                    $parser->recursiveTagParseFully( $linkText )
1700                );
1701            }
1702            [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
1703            $target = new TitleValue( NS_MAIN, $title, $frag, $prefix );
1704            $parser->getOutput()->addInterwikiLink( $target );
1705            return [
1706                'text' => Linker::link( $target, $linkText ),
1707                'isHTML' => true,
1708            ];
1709        }
1710        // Invalid interwiki link, render as plain text
1711        return [ 'found' => false ];
1712    }
1713
1714    public static function interlanguagelink( $parser, $prefix = '', $title = '', $linkText = null ) {
1715        $services = MediaWikiServices::getInstance();
1716        $extraInterlanguageLinkPrefixes = $services->getMainConfig()->get(
1717            MainConfigNames::ExtraInterlanguageLinkPrefixes
1718        );
1719        if (
1720            $prefix !== '' &&
1721            $services->getInterwikiLookup()->isValidInterwiki( $prefix ) &&
1722            (
1723                $services->getLanguageNameUtils()->getLanguageName(
1724                    $prefix, LanguageNameUtils::AUTONYMS, LanguageNameUtils::DEFINED
1725                ) || in_array( $prefix, $extraInterlanguageLinkPrefixes, true )
1726            )
1727        ) {
1728            // $linkText is ignored for language links, but fragment is kept
1729            [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
1730            $parser->getOutput()->addLanguageLink(
1731                new TitleValue(
1732                    NS_MAIN, $title, $frag, $prefix
1733                )
1734            );
1735            return '';
1736        }
1737        // Invalid language link, render as plain text
1738        return [ 'found' => false ];
1739    }
1740}
1741
1742/** @deprecated class alias since 1.43 */
1743class_alias( CoreParserFunctions::class, 'CoreParserFunctions' );