Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / parser / CoreParserFunctions.php
blob6b37697f72119536c20ca12604b79e1030fc4c5b
1 <?php
2 /**
3 * Parser functions provided by MediaWiki core
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.
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.
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
20 * @file
21 * @ingroup Parser
24 namespace MediaWiki\Parser;
26 use InvalidArgumentException;
27 use MediaWiki\Category\Category;
28 use MediaWiki\Config\ServiceOptions;
29 use MediaWiki\Language\Language;
30 use MediaWiki\Language\LanguageCode;
31 use MediaWiki\Languages\LanguageNameUtils;
32 use MediaWiki\Linker\Linker;
33 use MediaWiki\MainConfigNames;
34 use MediaWiki\MediaWikiServices;
35 use MediaWiki\Message\Message;
36 use MediaWiki\Revision\RevisionAccessException;
37 use MediaWiki\Revision\RevisionRecord;
38 use MediaWiki\SiteStats\SiteStats;
39 use MediaWiki\SpecialPage\SpecialPage;
40 use MediaWiki\Title\Title;
41 use MediaWiki\Title\TitleValue;
42 use MediaWiki\User\User;
43 use Wikimedia\Bcp47Code\Bcp47CodeValue;
44 use Wikimedia\RemexHtml\Tokenizer\Attributes;
45 use Wikimedia\RemexHtml\Tokenizer\PlainAttributes;
47 /**
48 * Various core parser functions, registered in every Parser
49 * @ingroup Parser
51 class CoreParserFunctions {
52 /** @var int Assume that no output will later be saved this many seconds after parsing */
53 private const MAX_TTS = 900;
55 /**
56 * @internal
58 public const REGISTER_OPTIONS = [
59 // See documentation for the corresponding config options
60 MainConfigNames::AllowDisplayTitle,
61 MainConfigNames::AllowSlowParserFunctions,
64 /**
65 * @param Parser $parser
66 * @param ServiceOptions $options
68 * @return void
69 * @internal
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 );
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.)
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',
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',
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',
132 foreach ( $noHashFunctions as $func ) {
133 $parser->setFunctionHook( $func, [ __CLASS__, $func ], Parser::SFH_NO_HASH );
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' ] );
142 if ( $allowDisplayTitle ) {
143 $parser->setFunctionHook(
144 'displaytitle',
145 [ __CLASS__, 'displaytitle' ],
146 Parser::SFH_NO_HASH
149 if ( $allowSlowParserFunctions ) {
150 $parser->setFunctionHook(
151 'pagesinnamespace',
152 [ __CLASS__, 'pagesinnamespace' ],
153 Parser::SFH_NO_HASH
159 * @param Parser $parser
160 * @param string $part1 Message key
161 * @param string ...$params To pass to wfMessage()
162 * @return array
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 ];
175 * @param Parser $parser
176 * @param string $date
177 * @param string|null $defaultPref
179 * @return string
181 public static function formatDate( $parser, $date, $defaultPref = null ) {
182 $lang = $parser->getTargetLanguage();
183 $df = MediaWikiServices::getInstance()->getDateFormatterFactory()->get( $lang );
185 $date = trim( $date );
187 $pref = $parser->getOptions()->getDateFormat();
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;
195 $date = $df->reformat( $pref, $date, [ 'match-whole' ] );
196 return $date;
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 ) );
205 if ( $index !== false ) {
206 return $parser->getContentLanguage()->getFormattedNsText( $index );
207 } else {
208 return [ 'found' => false ];
212 public static function nse( $parser, $part1 = '' ) {
213 $ret = self::ns( $parser, $part1 );
214 if ( is_string( $ret ) ) {
215 $ret = wfUrlencode( str_replace( ' ', '_', $ret ) );
217 return $ret;
221 * urlencodes a string according to one of three patterns: (T24474)
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 '_',
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
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' ] );
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;
245 // Encode for an HTTP Path, '%20' for ' '.
246 case 'url_path':
247 $func = 'rawurlencode';
248 break;
250 // Encode for HTTP query, '+' for ' '.
251 case 'url_query':
252 default:
253 $func = 'urlencode';
255 // See T105242, where the choice to kill markers and various
256 // other options were discussed.
257 return $func( $parser->killMarkers( $s ) );
260 public static function lcfirst( $parser, $s = '' ) {
261 return $parser->getContentLanguage()->lcfirst( $s );
264 public static function ucfirst( $parser, $s = '' ) {
265 return $parser->getContentLanguage()->ucfirst( $s );
269 * @param Parser $parser
270 * @param string $s
271 * @return string
273 public static function lc( $parser, $s = '' ) {
274 return $parser->markerSkipCallback( $s, [ $parser->getContentLanguage(), 'lc' ] );
278 * @param Parser $parser
279 * @param string $s
280 * @return string
282 public static function uc( $parser, $s = '' ) {
283 return $parser->markerSkipCallback( $s, [ $parser->getContentLanguage(), 'uc' ] );
286 public static function localurl( $parser, $s = '', $arg = null ) {
287 return self::urlFunction( 'getLocalURL', $s, $arg );
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 );
299 public static function fullurl( $parser, $s = '', $arg = null ) {
300 return self::urlFunction( 'getFullURL', $s, $arg );
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 );
312 public static function canonicalurl( $parser, $s = '', $arg = null ) {
313 return self::urlFunction( 'getCanonicalURL', $s, $arg );
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 );
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() );
336 if ( $arg !== null ) {
337 $text = $title->$func( $arg );
338 } else {
339 $text = $title->$func();
341 return $text;
342 } else {
343 return [ 'found' => false ];
348 * @param Parser $parser
349 * @param string $num
350 * @param string|null $arg
351 * @return string
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 )
359 $func = [ $parser->getTargetLanguage(), 'formatNumNoSeparators' ];
360 $func = self::getLegacyFormatNum( $parser, $func );
361 } else {
362 $func = [ $parser->getTargetLanguage(), 'formatNum' ];
363 $func = self::getLegacyFormatNum( $parser, $func );
365 return $parser->markerSkipCallback( $num, $func );
369 * @param Parser $parser
370 * @param callable $callback
372 * @return callable
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
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 );
394 return call_user_func( $callback, $number );
399 * @param Parser $parser
400 * @param string $case
401 * @param string $word
402 * @return string
404 public static function grammar( $parser, $case = '', $word = '' ) {
405 $word = $parser->killMarkers( $word );
406 return $parser->getTargetLanguage()->convertGrammar( $word, $case );
410 * @param Parser $parser
411 * @param string $username
412 * @param string ...$forms What to output for each gender
413 * @return string
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];
423 $username = trim( $username );
425 $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
426 $gender = $userOptionsLookup->getDefaultOption( 'gender' );
428 // allow prefix and normalize (e.g. "&#42;foo" -> "*foo" ).
429 $title = Title::newFromText( $username, NS_USER );
431 if ( $title && $title->inNamespace( NS_USER ) ) {
432 $username = $title->getText();
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__ );
443 $ret = $parser->getTargetLanguage()->gender( $gender, $forms );
444 return $ret;
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
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 );
460 public static function formal( Parser $parser, string ...$forms ): string {
461 $index = $parser->getTargetLanguage()->getFormalityIndex();
462 return $forms[$index] ?? $forms[0];
466 * @param Parser $parser
467 * @param string $text
468 * @return string
470 public static function bidi( $parser, $text = '' ) {
471 return $parser->getTargetLanguage()->embedBidi( $text );
475 * Override the title of the page when viewed, provided we've been given a
476 * title which will normalise to the canonical title
478 * @param Parser $parser Parent parser
479 * @param string $text Desired title text
480 * @param string $uarg
481 * @return string
483 public static function displaytitle( $parser, $text = '', $uarg = '' ) {
484 $restrictDisplayTitle = MediaWikiServices::getInstance()->getMainConfig()
485 ->get( MainConfigNames::RestrictDisplayTitle );
487 static $magicWords = null;
488 if ( $magicWords === null ) {
489 $magicWords = $parser->getMagicWordFactory()->newArray(
490 [ 'displaytitle_noerror', 'displaytitle_noreplace' ] );
492 $arg = $magicWords->matchStartToEnd( $uarg );
494 // parse a limited subset of wiki markup (just the single quote items)
495 $text = $parser->doQuotes( $text );
497 // remove stripped text (e.g. the UNIQ-QINU stuff) that was generated by tag extensions/whatever
498 $text = $parser->killMarkers( $text );
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' ];
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();
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'] );
518 if ( preg_match( '/(display|user-select|visibility)\s*:/i', $decoded['style'] ) ) {
519 $decoded['style'] = '/* attempt to bypass $wgRestrictDisplayTitle */';
523 return new PlainAttributes( $decoded );
525 } else {
526 $htmlTagsCallback = null;
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 );
540 if ( !$restrictDisplayTitle ||
541 ( $title instanceof Title
542 && !$title->hasFragment()
543 && $title->equals( $parser->getTitle() ) )
545 $old = $parser->getOutput()->getPageProperty( 'displaytitle' );
546 if ( $old === null || $arg !== 'displaytitle_noreplace' ) {
547 $parser->getOutput()->setDisplayTitle( $text );
549 if ( $old !== null && $old !== $text && !$arg ) {
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 '';
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 )
568 $parser->addTrackingCategory( 'restricted-displaytitle-ignored' );
573 * Matches the given value against the value of given magic word
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
581 private static function matchAgainstMagicword(
582 MagicWordFactory $magicWordFactory, $magicword, $value
584 $value = trim( strval( $value ) );
585 if ( $value === '' ) {
586 return false;
588 $mwObject = $magicWordFactory->get( $magicword );
589 return $mwObject->matchStartToEnd( $value );
593 * Formats a number according to a language.
595 * @param int|float $num
596 * @param ?string $raw
597 * @param Language $language
598 * @param MagicWordFactory|null $magicWordFactory To evaluate $raw
599 * @return string
601 public static function formatRaw(
602 $num, $raw, $language, ?MagicWordFactory $magicWordFactory = null
604 if ( $raw !== null && $raw !== '' ) {
605 if ( !$magicWordFactory ) {
606 $magicWordFactory = MediaWikiServices::getInstance()->getMagicWordFactory();
608 if ( self::matchAgainstMagicword( $magicWordFactory, 'rawsuffix', $raw ) ) {
609 return (string)$num;
612 return $language->formatNum( $num );
615 public static function numberofpages( $parser, $raw = null ) {
616 return self::formatRaw( SiteStats::pages(), $raw, $parser->getTargetLanguage() );
619 public static function numberofusers( $parser, $raw = null ) {
620 return self::formatRaw( SiteStats::users(), $raw, $parser->getTargetLanguage() );
623 public static function numberofactiveusers( $parser, $raw = null ) {
624 return self::formatRaw( SiteStats::activeUsers(), $raw, $parser->getTargetLanguage() );
627 public static function numberofarticles( $parser, $raw = null ) {
628 return self::formatRaw( SiteStats::articles(), $raw, $parser->getTargetLanguage() );
631 public static function numberoffiles( $parser, $raw = null ) {
632 return self::formatRaw( SiteStats::images(), $raw, $parser->getTargetLanguage() );
635 public static function numberofadmins( $parser, $raw = null ) {
636 return self::formatRaw(
637 SiteStats::numberingroup( 'sysop' ),
638 $raw,
639 $parser->getTargetLanguage()
643 public static function numberofedits( $parser, $raw = null ) {
644 return self::formatRaw( SiteStats::edits(), $raw, $parser->getTargetLanguage() );
647 public static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) {
648 return self::formatRaw(
649 SiteStats::pagesInNs( intval( $namespace ) ),
650 $raw,
651 $parser->getTargetLanguage()
655 public static function numberingroup( $parser, $name = '', $raw = null ) {
656 return self::formatRaw(
657 SiteStats::numberingroup( strtolower( $name ) ),
658 $raw,
659 $parser->getTargetLanguage()
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
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 );
678 return $title;
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
689 public static function namespace( $parser, $title = null ) {
690 $t = self::makeTitle( $parser, $title );
691 if ( $t === null ) {
692 return '';
694 return str_replace( '_', ' ', $t->getNsText() );
697 public static function namespacee( $parser, $title = null ) {
698 $t = self::makeTitle( $parser, $title );
699 if ( $t === null ) {
700 return '';
702 return wfUrlencode( $t->getNsText() );
705 public static function namespacenumber( $parser, $title = null ) {
706 $t = self::makeTitle( $parser, $title );
707 if ( $t === null ) {
708 return '';
710 return (string)$t->getNamespace();
713 public static function talkspace( $parser, $title = null ) {
714 $t = self::makeTitle( $parser, $title );
715 if ( $t === null || !$t->canHaveTalkPage() ) {
716 return '';
718 return str_replace( '_', ' ', $t->getTalkNsText() );
721 public static function talkspacee( $parser, $title = null ) {
722 $t = self::makeTitle( $parser, $title );
723 if ( $t === null || !$t->canHaveTalkPage() ) {
724 return '';
726 return wfUrlencode( $t->getTalkNsText() );
729 public static function subjectspace( $parser, $title = null ) {
730 $t = self::makeTitle( $parser, $title );
731 if ( $t === null ) {
732 return '';
734 return str_replace( '_', ' ', $t->getSubjectNsText() );
737 public static function subjectspacee( $parser, $title = null ) {
738 $t = self::makeTitle( $parser, $title );
739 if ( $t === null ) {
740 return '';
742 return wfUrlencode( $t->getSubjectNsText() );
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
752 public static function pagename( $parser, $title = null ) {
753 $t = self::makeTitle( $parser, $title );
754 if ( $t === null ) {
755 return '';
757 return wfEscapeWikiText( $t->getText() );
760 public static function pagenamee( $parser, $title = null ) {
761 $t = self::makeTitle( $parser, $title );
762 if ( $t === null ) {
763 return '';
765 return wfEscapeWikiText( $t->getPartialURL() );
768 public static function fullpagename( $parser, $title = null ) {
769 $t = self::makeTitle( $parser, $title );
770 if ( $t === null ) {
771 return '';
773 return wfEscapeWikiText( $t->getPrefixedText() );
776 public static function fullpagenamee( $parser, $title = null ) {
777 $t = self::makeTitle( $parser, $title );
778 if ( $t === null ) {
779 return '';
781 return wfEscapeWikiText( $t->getPrefixedURL() );
784 public static function subpagename( $parser, $title = null ) {
785 $t = self::makeTitle( $parser, $title );
786 if ( $t === null ) {
787 return '';
789 return wfEscapeWikiText( $t->getSubpageText() );
792 public static function subpagenamee( $parser, $title = null ) {
793 $t = self::makeTitle( $parser, $title );
794 if ( $t === null ) {
795 return '';
797 return wfEscapeWikiText( $t->getSubpageUrlForm() );
800 public static function rootpagename( $parser, $title = null ) {
801 $t = self::makeTitle( $parser, $title );
802 if ( $t === null ) {
803 return '';
805 return wfEscapeWikiText( $t->getRootText() );
808 public static function rootpagenamee( $parser, $title = null ) {
809 $t = self::makeTitle( $parser, $title );
810 if ( $t === null ) {
811 return '';
813 return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getRootText() ) ) );
816 public static function basepagename( $parser, $title = null ) {
817 $t = self::makeTitle( $parser, $title );
818 if ( $t === null ) {
819 return '';
821 return wfEscapeWikiText( $t->getBaseText() );
824 public static function basepagenamee( $parser, $title = null ) {
825 $t = self::makeTitle( $parser, $title );
826 if ( $t === null ) {
827 return '';
829 return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getBaseText() ) ) );
832 public static function talkpagename( $parser, $title = null ) {
833 $t = self::makeTitle( $parser, $title );
834 if ( $t === null || !$t->canHaveTalkPage() ) {
835 return '';
837 return wfEscapeWikiText( $t->getTalkPage()->getPrefixedText() );
840 public static function talkpagenamee( $parser, $title = null ) {
841 $t = self::makeTitle( $parser, $title );
842 if ( $t === null || !$t->canHaveTalkPage() ) {
843 return '';
845 return wfEscapeWikiText( $t->getTalkPage()->getPrefixedURL() );
848 public static function subjectpagename( $parser, $title = null ) {
849 $t = self::makeTitle( $parser, $title );
850 if ( $t === null ) {
851 return '';
853 return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedText() );
856 public static function subjectpagenamee( $parser, $title = null ) {
857 $t = self::makeTitle( $parser, $title );
858 if ( $t === null ) {
859 return '';
861 return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedURL() );
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
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 ] );
884 static $cache = [];
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;
896 if ( !$type ) { // backward compatibility
897 $type = 'pagesincategory_all';
900 $title = Title::makeTitleSafe( NS_CATEGORY, $name );
901 if ( !$title ) { # invalid title
902 return self::formatRaw( 0, $raw, $parser->getTargetLanguage() );
904 $languageConverter = MediaWikiServices::getInstance()
905 ->getLanguageConverterFactory()
906 ->getLanguageConverter( $parser->getContentLanguage() );
907 $languageConverter->findVariantLink( $name, $title, true );
909 // Normalize name for cache
910 $name = $title->getDBkey();
912 if ( !isset( $cache[$name] ) ) {
913 $category = Category::newFromTitle( $title );
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 );
922 $cache[$name]['pagesincategory_all'] = $allCount;
923 $cache[$name]['pagesincategory_pages'] = $pageCount;
924 $cache[$name]['pagesincategory_subcats'] = $subcatCount;
925 $cache[$name]['pagesincategory_files'] = $fileCount;
928 $count = $cache[$name][$type];
929 return self::formatRaw( $count, $raw, $parser->getTargetLanguage() );
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.
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
941 public static function pagesize( $parser, $page = '', $raw = null ) {
942 $title = Title::newFromText( $page );
944 if ( !is_object( $title ) || $title->isExternal() ) {
945 return self::formatRaw( 0, $raw, $parser->getTargetLanguage() );
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;
955 return self::formatRaw( $length, $raw, $parser->getTargetLanguage() );
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
964 * @param Parser $parser
965 * @param string $type
966 * @param string $title
968 * @return string
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 );
979 return '';
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
988 * @param Parser $parser
989 * @param string $type
990 * @param string $title
992 * @return string
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 ) ) ?? '';
1002 return '';
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
1013 public static function language( $parser, $code = '', $inLanguage = '' ) {
1014 if ( $code === '' ) {
1015 $code = $parser->getTargetLanguage()->getCode();
1017 if ( $inLanguage === '' ) {
1018 $inLanguage = LanguageNameUtils::AUTONYMS;
1020 $lang = MediaWikiServices::getInstance()
1021 ->getLanguageNameUtils()
1022 ->getLanguageName( $code, $inLanguage );
1023 return $lang !== '' ? $lang : LanguageCode::bcp47( $code );
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'
1037 public static function dir( Parser $parser, string $code = '', string $arg = '' ): string {
1038 static $magicWords = null;
1039 $languageFactory = MediaWikiServices::getInstance()->getLanguageFactory();
1041 if ( $code === '' ) {
1042 $lang = $parser->getTargetLanguage();
1043 } else {
1044 if ( $arg !== '' ) {
1045 if ( $magicWords === null ) {
1046 $magicWords = $parser->getMagicWordFactory()->newArray( [ 'language_option_bcp47' ] );
1048 if ( $magicWords->matchStartToEnd( $arg ) === 'language_option_bcp47' ) {
1049 // Prefer the BCP-47 interpretation of this code.
1050 $code = new Bcp47CodeValue( $code );
1053 try {
1054 $lang = $languageFactory->getLanguage( $code );
1055 } catch ( InvalidArgumentException $ex ) {
1056 $parser->addTrackingCategory( 'bad-language-code-category' );
1057 return 'ltr';
1060 return $lang->getDir();
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
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 );
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
1088 public static function pad(
1089 $parser, $string, $length, $padding = '0', $direction = STR_PAD_RIGHT
1091 $padding = $parser->killMarkers( $padding );
1092 $lengthOfPadding = mb_strlen( $padding );
1093 if ( $lengthOfPadding == 0 ) {
1094 return $string;
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;
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;
1114 if ( $direction == STR_PAD_LEFT ) {
1115 return $finalPadding . $string;
1116 } else {
1117 return $string . $finalPadding;
1121 public static function padleft( $parser, $string = '', $length = 0, $padding = '0' ) {
1122 return self::pad( $parser, $string, $length, $padding, STR_PAD_LEFT );
1125 public static function padright( $parser, $string = '', $length = 0, $padding = '0' ) {
1126 return self::pad( $parser, $string, $length, $padding );
1130 * @param Parser $parser
1131 * @param string $text
1132 * @return string
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 );
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' );
1153 public static function speciale( $parser, $text ) {
1154 return wfUrlencode( str_replace( ' ', '_', self::special( $parser, $text ) ) );
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
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' ] );
1171 $arg = $magicWords->matchStartToEnd( $uarg );
1173 $text = trim( $text );
1174 if ( strlen( $text ) == 0 ) {
1175 return '';
1177 $old = $parser->getOutput()->getPageProperty( 'defaultsort' );
1178 if ( $old === null || $arg !== 'defaultsort_noreplace' ) {
1179 $parser->getOutput()->setPageProperty( 'defaultsort', $text );
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>';
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}}.
1201 * @param Parser $parser
1202 * @param string $name
1203 * @param string $argA
1204 * @param string $argB
1205 * @return array|string
1207 public static function filepath( $parser, $name = '', $argA = '', $argB = '' ) {
1208 $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $name );
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' );
1220 if ( $file ) {
1221 $url = $file->getFullUrl();
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;
1233 if ( $isNowiki ) {
1234 return [ $url, 'nowiki' => true ];
1236 return $url;
1237 } else {
1238 return '';
1243 * Parser function to extension tag adaptor
1244 * @param Parser $parser
1245 * @param PPFrame $frame
1246 * @param PPNode[] $args
1247 * @return string
1249 public static function tagObj( $parser, $frame, $args ) {
1250 if ( !count( $args ) ) {
1251 return '';
1253 $tagName = strtolower( trim( $frame->expand( array_shift( $args ) ) ) );
1254 $processNowiki = $parser->tagNeedsNowikiStrippedInTagPF( $tagName ) ? PPFrame::PROCESS_NOWIKI : 0;
1256 if ( count( $args ) ) {
1257 $inner = $frame->expand( array_shift( $args ), $processNowiki );
1258 } else {
1259 $inner = null;
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] ?? '';
1271 $attributes[$name] = $value;
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 ) . '"';
1283 if ( $inner === null ) {
1284 return "<$tagName$attrText/>";
1286 return "<$tagName$attrText>$inner</$tagName>";
1289 $params = [
1290 'name' => $tagName,
1291 'inner' => $inner,
1292 'attributes' => $attributes,
1293 'close' => "</$tagName>",
1295 return $parser->extensionSubstitution( $params, $frame );
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
1305 * @param Parser $parser
1306 * @param Title $title
1307 * @param string $vary ParserOutput vary-* flag
1308 * @return RevisionRecord|null
1309 * @since 1.23
1311 private static function getCachedRevisionObject( $parser, $title, $vary ) {
1312 if ( !$title ) {
1313 return null;
1316 $revisionRecord = null;
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.
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;
1335 $parserOutput = $parser->getOutput();
1336 if ( !$revisionRecord ) {
1337 if (
1338 !$parser->isCurrentRevisionOfTitleCached( $title ) &&
1339 !$parser->incrementExpensiveFunctionCount()
1341 return null; // not allowed
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;
1349 // Register dependency in templatelinks
1350 $parserOutput->addTemplate(
1351 $title,
1352 $revisionRecord ? $revisionRecord->getPageId() : 0,
1353 $revisionRecord ? $revisionRecord->getId() : 0
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;
1367 $parserOutput->setRevisionUsedSha1Base36( $sha1 );
1371 return $revisionRecord;
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
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
1389 $parserOutput = $parser->getOutput();
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 );
1400 return $id;
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 );
1410 return $id;
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 );
1418 return $id;
1421 return null;
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
1431 public static function revisionid( $parser, $title = null ) {
1432 $t = self::makeTitle( $parser, $title );
1433 if ( $t === null || $t->isExternal() ) {
1434 return '';
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() )
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 '';
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();
1469 if ( !$id ) {
1470 $id = $parser->getOptions()->getSpeculativeRevId();
1471 if ( $id ) {
1472 $parser->getOutput()->setSpeculativeRevIdUsed( $id );
1475 return (string)$id;
1477 $rev = self::getCachedRevisionObject( $parser, $t, ParserOutputFlags::VARY_REVISION_ID );
1478 return $rev ? $rev->getId() : '';
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
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 );
1516 return $resNow;
1517 } else {
1518 $rev = self::getCachedRevisionObject( $parser, $title, ParserOutputFlags::VARY_REVISION_TIMESTAMP );
1519 if ( !$rev ) {
1520 return '';
1522 $resNow = substr(
1523 $parser->getContentLanguage()->userAdjust( $rev->getTimestamp(), '' ), $start, $len
1525 return $resNow;
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
1536 public static function revisionday( $parser, $title = null ) {
1537 $t = self::makeTitle( $parser, $title );
1538 if ( $t === null || $t->isExternal() ) {
1539 return '';
1541 return strval( (int)self::getRevisionTimestampSubstring(
1542 $parser, $t, 6, 2, self::MAX_TTS
1543 ) );
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
1553 public static function revisionday2( $parser, $title = null ) {
1554 $t = self::makeTitle( $parser, $title );
1555 if ( $t === null || $t->isExternal() ) {
1556 return '';
1558 return self::getRevisionTimestampSubstring(
1559 $parser, $t, 6, 2, self::MAX_TTS
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
1570 public static function revisionmonth( $parser, $title = null ) {
1571 $t = self::makeTitle( $parser, $title );
1572 if ( $t === null || $t->isExternal() ) {
1573 return '';
1575 return self::getRevisionTimestampSubstring(
1576 $parser, $t, 4, 2, self::MAX_TTS
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
1587 public static function revisionmonth1( $parser, $title = null ) {
1588 $t = self::makeTitle( $parser, $title );
1589 if ( $t === null || $t->isExternal() ) {
1590 return '';
1592 return strval( (int)self::getRevisionTimestampSubstring(
1593 $parser, $t, 4, 2, self::MAX_TTS
1594 ) );
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
1604 public static function revisionyear( $parser, $title = null ) {
1605 $t = self::makeTitle( $parser, $title );
1606 if ( $t === null || $t->isExternal() ) {
1607 return '';
1609 return self::getRevisionTimestampSubstring(
1610 $parser, $t, 0, 4, self::MAX_TTS
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
1621 public static function revisiontimestamp( $parser, $title = null ) {
1622 $t = self::makeTitle( $parser, $title );
1623 if ( $t === null || $t->isExternal() ) {
1624 return '';
1626 return self::getRevisionTimestampSubstring(
1627 $parser, $t, 0, 14, self::MAX_TTS
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
1638 public static function revisionuser( $parser, $title = null ) {
1639 $t = self::makeTitle( $parser, $title );
1640 if ( $t === null || $t->isExternal() ) {
1641 return '';
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();
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() : '';
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.
1666 * @param Parser $parser
1667 * @param ?string $title
1669 * @return string
1670 * @since 1.23
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()
1678 $names = [];
1679 $sources = $restrictionStore->getCascadeProtectionSources( $titleObject );
1680 $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
1681 foreach ( $sources[0] as $sourcePageIdentity ) {
1682 $names[] = $titleFormatter->getPrefixedText( $sourcePageIdentity );
1684 return implode( '|', $names );
1686 return '';
1689 public static function interwikilink( $parser, $prefix = '', $title = '', $linkText = null ) {
1690 $services = MediaWikiServices::getInstance();
1691 if (
1692 $prefix !== '' &&
1693 $services->getInterwikiLookup()->isValidInterwiki( $prefix )
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 )
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,
1710 // Invalid interwiki link, render as plain text
1711 return [ 'found' => false ];
1714 public static function interlanguagelink( $parser, $prefix = '', $title = '', $linkText = null ) {
1715 $services = MediaWikiServices::getInstance();
1716 $extraInterlanguageLinkPrefixes = $services->getMainConfig()->get(
1717 MainConfigNames::ExtraInterlanguageLinkPrefixes
1719 if (
1720 $prefix !== '' &&
1721 $services->getInterwikiLookup()->isValidInterwiki( $prefix ) &&
1723 $services->getLanguageNameUtils()->getLanguageName(
1724 $prefix, LanguageNameUtils::AUTONYMS, LanguageNameUtils::DEFINED
1725 ) || in_array( $prefix, $extraInterlanguageLinkPrefixes, true )
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
1735 return '';
1737 // Invalid language link, render as plain text
1738 return [ 'found' => false ];
1742 /** @deprecated class alias since 1.43 */
1743 class_alias( CoreParserFunctions::class, 'CoreParserFunctions' );