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