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
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
;
48 * Various core parser functions, registered in every 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;
58 public const REGISTER_OPTIONS
= [
59 // See documentation for the corresponding config options
60 MainConfigNames
::AllowDisplayTitle
,
61 MainConfigNames
::AllowSlowParserFunctions
,
65 * @param Parser $parser
66 * @param ServiceOptions $options
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",
79 # optional Parser::SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}}
80 # instead of {{#int:...}})
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',
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',
120 'numberofactiveusers',
125 # These magic words already contain the hash, and the no-args form
126 # is the same as passing an empty first argument
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(
145 [ __CLASS__
, 'displaytitle' ],
149 if ( $allowSlowParserFunctions ) {
150 $parser->setFunctionHook(
152 [ __CLASS__
, 'pagesinnamespace' ],
159 * @param Parser $parser
160 * @param string $part1 Message key
161 * @param string ...$params To pass to wfMessage()
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 ];
170 return [ 'found' => false ];
175 * @param Parser $parser
176 * @param string $date
177 * @param string|null $defaultPref
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' ] );
199 public static function ns( $parser, $part1 = '' ) {
200 if ( intval( $part1 ) ||
$part1 == "0" ) {
201 $index = intval( $part1 );
203 $index = $parser->getContentLanguage()->getNsIndex( str_replace( ' ', '_', $part1 ) );
205 if ( $index !== false ) {
206 return $parser->getContentLanguage()->getFormattedNsText( $index );
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 ) );
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.
232 public static function urlencode( $parser, $s = '', $arg = null ) {
233 static $magicWords = null;
234 if ( $magicWords === null ) {
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 ' '.
241 $func = 'wfUrlencode';
242 $s = str_replace( ' ', '_', $s );
245 // Encode for an HTTP Path, '%20' for ' '.
247 $func = 'rawurlencode';
250 // Encode for HTTP query, '+' for ' '.
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
273 public static function lc( $parser, $s = '' ) {
274 return $parser->markerSkipCallback( $s, [ $parser->getContentLanguage(), 'lc' ] );
278 * @param Parser $parser
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 ) ) {
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 ) ) {
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 ) ) {
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 );
339 $text = $title->$func();
343 return [ 'found' => false ];
348 * @param Parser $parser
350 * @param string|null $arg
353 public static function formatnum( $parser, $num = '', $arg = null ) {
354 if ( self
::matchAgainstMagicword( $parser->getMagicWordFactory(), 'rawsuffix', $arg ) ) {
355 $func = [ $parser->getTargetLanguage(), 'parseFormattedNumber' ];
357 self
::matchAgainstMagicword( $parser->getMagicWordFactory(), 'nocommafysuffix', $arg )
359 $func = [ $parser->getTargetLanguage(), 'formatNumNoSeparators' ];
360 $func = self
::getLegacyFormatNum( $parser, $func );
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
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+)?';
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] );
394 return call_user_func( $callback, $number );
399 * @param Parser $parser
400 * @param string $case
401 * @param string $word
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
415 public static function gender( $parser, $username, ...$forms ) {
416 // Some shortcuts to avoid loading user data unnecessarily
417 if ( count( $forms ) === 0 ) {
419 } elseif ( count( $forms ) === 1 ) {
423 $username = trim( $username );
425 $userOptionsLookup = MediaWikiServices
::getInstance()->getUserOptionsLookup();
426 $gender = $userOptionsLookup->getDefaultOption( 'gender' );
428 // allow prefix and normalize (e.g. "*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();
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 );
448 * @param Parser $parser
449 * @param string $text
450 * @param string ...$forms What to output for each number (singular, dual, plural, etc.)
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
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
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
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 );
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,
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 ) )
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 === '' ) {
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
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 ) ) {
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' ),
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 ) ),
651 $parser->getTargetLanguage()
655 public static function numberingroup( $parser, $name = '', $raw = null ) {
656 return self
::formatRaw(
657 SiteStats
::numberingroup( strtolower( $name ) ),
659 $parser->getTargetLanguage()
664 * Helper function for preprocessing an optional argument which represents
666 * @param Parser $parser
667 * @param string|null $t if null, returns the Parser's Title
668 * for consistency with magic variable forms
671 private static function makeTitle( Parser
$parser, ?
string $t ) {
673 // For consistency with magic variable forms
674 $title = $parser->getTitle();
676 $title = Title
::newFromText( $t );
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
689 public static function namespace( $parser, $title = null ) {
690 $t = self
::makeTitle( $parser, $title );
694 return str_replace( '_', ' ', $t->getNsText() );
697 public static function namespacee( $parser, $title = null ) {
698 $t = self
::makeTitle( $parser, $title );
702 return wfUrlencode( $t->getNsText() );
705 public static function namespacenumber( $parser, $title = null ) {
706 $t = self
::makeTitle( $parser, $title );
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() ) {
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() ) {
726 return wfUrlencode( $t->getTalkNsText() );
729 public static function subjectspace( $parser, $title = null ) {
730 $t = self
::makeTitle( $parser, $title );
734 return str_replace( '_', ' ', $t->getSubjectNsText() );
737 public static function subjectspacee( $parser, $title = null ) {
738 $t = self
::makeTitle( $parser, $title );
742 return wfUrlencode( $t->getSubjectNsText() );
746 * Functions to get and normalize pagenames, corresponding to the magic words
748 * @param Parser $parser
749 * @param string|null $title
752 public static function pagename( $parser, $title = null ) {
753 $t = self
::makeTitle( $parser, $title );
757 return wfEscapeWikiText( $t->getText() );
760 public static function pagenamee( $parser, $title = null ) {
761 $t = self
::makeTitle( $parser, $title );
765 return wfEscapeWikiText( $t->getPartialURL() );
768 public static function fullpagename( $parser, $title = null ) {
769 $t = self
::makeTitle( $parser, $title );
773 return wfEscapeWikiText( $t->getPrefixedText() );
776 public static function fullpagenamee( $parser, $title = null ) {
777 $t = self
::makeTitle( $parser, $title );
781 return wfEscapeWikiText( $t->getPrefixedURL() );
784 public static function subpagename( $parser, $title = null ) {
785 $t = self
::makeTitle( $parser, $title );
789 return wfEscapeWikiText( $t->getSubpageText() );
792 public static function subpagenamee( $parser, $title = null ) {
793 $t = self
::makeTitle( $parser, $title );
797 return wfEscapeWikiText( $t->getSubpageUrlForm() );
800 public static function rootpagename( $parser, $title = null ) {
801 $t = self
::makeTitle( $parser, $title );
805 return wfEscapeWikiText( $t->getRootText() );
808 public static function rootpagenamee( $parser, $title = null ) {
809 $t = self
::makeTitle( $parser, $title );
813 return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getRootText() ) ) );
816 public static function basepagename( $parser, $title = null ) {
817 $t = self
::makeTitle( $parser, $title );
821 return wfEscapeWikiText( $t->getBaseText() );
824 public static function basepagenamee( $parser, $title = null ) {
825 $t = self
::makeTitle( $parser, $title );
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() ) {
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() ) {
845 return wfEscapeWikiText( $t->getTalkPage()->getPrefixedURL() );
848 public static function subjectpagename( $parser, $title = null ) {
849 $t = self
::makeTitle( $parser, $title );
853 return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedText() );
856 public static function subjectpagenamee( $parser, $title = null ) {
857 $t = self
::makeTitle( $parser, $title );
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
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'
886 // split the given option to its variable
887 if ( self
::matchAgainstMagicword( $parser->getMagicWordFactory(), 'rawsuffix', $arg1 ) ) {
888 // {{pagesincategory:|raw[|type]}}
890 $type = $magicWords->matchStartToEnd( $arg2 );
892 // {{pagesincategory:[|type[|raw]]}}
893 $type = $magicWords->matchStartToEnd( $arg1 );
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
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
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
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 );
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
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 ) ) ??
'';
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.
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
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();
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 );
1054 $lang = $languageFactory->getLanguage( $code );
1055 } catch ( InvalidArgumentException
$ex ) {
1056 $parser->addTrackingCategory( 'bad-language-code-category' );
1060 return $lang->getDir();
1064 * Gives the BCP-47 code for a language given the mediawiki internal
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();
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
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 ) {
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 ) {
1104 # $finalPadding is just $padding repeated enough times so that
1105 # mb_strlen( $string ) + mb_strlen( $finalPadding ) == $length
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;
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
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 );
1144 $title = SpecialPage
::getTitleFor( $page, $subpage );
1145 return $title->getPrefixedText();
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.
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 ) {
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 ) {
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 ) )
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] }}
1213 $parsedWidthParam = $parser->parseWidthParam( $argB );
1215 // {{filepath: [| size [|option]] }}
1216 $parsedWidthParam = $parser->parseWidthParam( $argA );
1217 $isNowiki = ( $argB == 'nowiki' );
1221 $url = $file->getFullUrl();
1223 // If a size is requested...
1224 if ( count( $parsedWidthParam ) ) {
1225 $mto = $file->transform( $parsedWidthParam );
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;
1234 return [ $url, 'nowiki' => true ];
1243 * Parser function to extension tag adaptor
1244 * @param Parser $parser
1245 * @param PPFrame $frame
1246 * @param PPNode[] $args
1249 public static function tagObj( $parser, $frame, $args ) {
1250 if ( !count( $args ) ) {
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 );
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
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>";
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
1311 private static function getCachedRevisionObject( $parser, $title, $vary ) {
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 ) {
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(
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 ) {
1363 $sha1 = $revisionRecord->getSha1();
1364 } catch ( RevisionAccessException
$e ) {
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
1381 public static function pageid( $parser, $title = null ) {
1382 $t = self
::makeTitle( $parser, $title );
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();
1397 $parserOutput->setSpeculativePageIdUsed( $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 );
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 );
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
1431 public static function revisionid( $parser, $title = null ) {
1432 $t = self
::makeTitle( $parser, $title );
1433 if ( $t === null ||
$t->isExternal() ) {
1437 $services = MediaWikiServices
::getInstance();
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() ) {
1450 $parser->getOutput()->setOutputFlag( ParserOutputFlags
::VARY_REVISION_EXISTS
);
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
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();
1464 $rev = $parser->getRevisionRecordObject();
1466 $id = $rev->getId();
1470 $id = $parser->getOptions()->getSpeculativeRevId();
1472 $parser->getOutput()->setSpeculativeRevIdUsed( $id );
1477 $rev = self
::getCachedRevisionObject( $parser, $t, ParserOutputFlags
::VARY_REVISION_ID
);
1478 return $rev ?
$rev->getId() : '';
1481 private static function getRevisionTimestampSubstring(
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.
1504 $parser->getContentLanguage()->userAdjust( wfTimestamp( TS_MW
, time() +
$mtts ), '' ),
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
);
1518 $rev = self
::getCachedRevisionObject( $parser, $title, ParserOutputFlags
::VARY_REVISION_TIMESTAMP
);
1523 $parser->getContentLanguage()->userAdjust( $rev->getTimestamp(), '' ), $start, $len
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
1536 public static function revisionday( $parser, $title = null ) {
1537 $t = self
::makeTitle( $parser, $title );
1538 if ( $t === null ||
$t->isExternal() ) {
1541 return strval( (int)self
::getRevisionTimestampSubstring(
1542 $parser, $t, 6, 2, self
::MAX_TTS
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
1553 public static function revisionday2( $parser, $title = null ) {
1554 $t = self
::makeTitle( $parser, $title );
1555 if ( $t === null ||
$t->isExternal() ) {
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
1570 public static function revisionmonth( $parser, $title = null ) {
1571 $t = self
::makeTitle( $parser, $title );
1572 if ( $t === null ||
$t->isExternal() ) {
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
1587 public static function revisionmonth1( $parser, $title = null ) {
1588 $t = self
::makeTitle( $parser, $title );
1589 if ( $t === null ||
$t->isExternal() ) {
1592 return strval( (int)self
::getRevisionTimestampSubstring(
1593 $parser, $t, 4, 2, self
::MAX_TTS
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
1604 public static function revisionyear( $parser, $title = null ) {
1605 $t = self
::makeTitle( $parser, $title );
1606 if ( $t === null ||
$t->isExternal() ) {
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
1621 public static function revisiontimestamp( $parser, $title = null ) {
1622 $t = self
::makeTitle( $parser, $title );
1623 if ( $t === null ||
$t->isExternal() ) {
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
1638 public static function revisionuser( $parser, $title = null ) {
1639 $t = self
::makeTitle( $parser, $title );
1640 if ( $t === null ||
$t->isExternal() ) {
1643 // VARY_USER informs the edit saving system that getting the canonical
1644 // output after revision insertion requires a parse that used the
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
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()
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 );
1689 public static function interwikilink( $parser, $prefix = '', $title = '', $linkText = null ) {
1690 $services = MediaWikiServices
::getInstance();
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 );
1706 'text' => Linker
::link( $target, $linkText ),
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
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(
1732 NS_MAIN
, $title, $frag, $prefix
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' );