MediaWiki master
CoreParserFunctions.php
Go to the documentation of this file.
1<?php
10namespace MediaWiki\Parser;
11
12use InvalidArgumentException;
18use MediaWiki\Languages\LanguageNameUtils;
30use Wikimedia\Bcp47Code\Bcp47CodeValue;
31use Wikimedia\RemexHtml\Tokenizer\Attributes;
32use Wikimedia\RemexHtml\Tokenizer\PlainAttributes;
33
40 private const MAX_TTS = 900;
41
45 public const REGISTER_OPTIONS = [
46 // See documentation for the corresponding config options
49 ];
50
58 public static function register( Parser $parser, ServiceOptions $options ) {
59 $options->assertRequiredOptions( self::REGISTER_OPTIONS );
60 $allowDisplayTitle = $options->get( MainConfigNames::AllowDisplayTitle );
61 $allowSlowParserFunctions = $options->get( MainConfigNames::AllowSlowParserFunctions );
62
63 # Syntax for arguments (see Parser::setFunctionHook):
64 # "name for lookup in localized magic words array",
65 # function callback,
66 # optional Parser::SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}}
67 # instead of {{#int:...}})
68 $noHashFunctions = [
69 'ns', 'nse', 'urlencode', 'lcfirst', 'ucfirst', 'lc', 'uc',
70 'localurl', 'localurle', 'fullurl', 'fullurle', 'canonicalurl',
71 'canonicalurle', 'formatnum', 'grammar', 'gender', 'plural', 'formal',
72 'bidi', 'numberingroup', 'language',
73 'padleft', 'padright', 'anchorencode', 'defaultsort', 'filepath',
74 'pagesincategory', 'pagesize', 'protectionlevel', 'protectionexpiry',
75 # The following are the "parser function" forms of magic
76 # variables defined in CoreMagicVariables. The no-args form will
77 # go through the magic variable code path (and be cached); the
78 # presence of arguments will cause the parser function form to
79 # be invoked. (Note that the actual implementation will pass
80 # a Parser object as first argument, in addition to the
81 # parser function parameters.)
82
83 # For this group, the first parameter to the parser function is
84 # "page title", and the no-args form (and the magic variable)
85 # defaults to "current page title".
86 'pagename', 'pagenamee',
87 'fullpagename', 'fullpagenamee',
88 'subpagename', 'subpagenamee',
89 'rootpagename', 'rootpagenamee',
90 'basepagename', 'basepagenamee',
91 'talkpagename', 'talkpagenamee',
92 'subjectpagename', 'subjectpagenamee',
93 'pageid', 'revisionid', 'revisionday',
94 'revisionday2', 'revisionmonth', 'revisionmonth1', 'revisionyear',
95 'revisiontimestamp',
96 'revisionuser',
97 'cascadingsources',
98 'namespace', 'namespacee', 'namespacenumber', 'talkspace', 'talkspacee',
99 'subjectspace', 'subjectspacee',
100
101 # More parser functions corresponding to CoreMagicVariables.
102 # For this group, the first parameter to the parser function is
103 # "raw" (uses the 'raw' format if present) and the no-args form
104 # (and the magic variable) defaults to 'not raw'.
105 'numberofarticles', 'numberoffiles',
106 'numberofusers',
107 'numberofactiveusers',
108 'numberofpages',
109 'numberofadmins',
110 'numberofedits',
111
112 # These magic words already contain the hash, and the no-args form
113 # is the same as passing an empty first argument
114 'bcp47',
115 'dir',
116 'interwikilink',
117 'interlanguagelink',
118 'contentmodel',
119 ];
120 foreach ( $noHashFunctions as $func ) {
121 $parser->setFunctionHook( $func, [ self::class, $func ], Parser::SFH_NO_HASH );
122 }
123
124 $parser->setFunctionHook( 'int', [ self::class, 'intFunction' ], Parser::SFH_NO_HASH );
125 $parser->setFunctionHook( 'special', [ self::class, 'special' ] );
126 $parser->setFunctionHook( 'speciale', [ self::class, 'speciale' ] );
127 $parser->setFunctionHook( 'tag', [ self::class, 'tagObj' ], Parser::SFH_OBJECT_ARGS );
128 $parser->setFunctionHook( 'formatdate', [ self::class, 'formatDate' ] );
129
130 if ( $allowDisplayTitle ) {
131 $parser->setFunctionHook(
132 'displaytitle',
133 [ self::class, 'displaytitle' ],
135 );
136 }
137 if ( $allowSlowParserFunctions ) {
138 $parser->setFunctionHook(
139 'pagesinnamespace',
140 [ self::class, 'pagesinnamespace' ],
142 );
143 }
144 }
145
152 public static function intFunction( $parser, $part1 = '', ...$params ) {
153 if ( strval( $part1 ) !== '' ) {
154 $message = wfMessage( $part1, $params )
155 ->inLanguage( $parser->getOptions()->getUserLangObj() );
156 return [ $message->plain(), 'noparse' => false ];
157 } else {
158 return [ 'found' => false ];
159 }
160 }
161
169 public static function formatDate( $parser, $date, $defaultPref = null ) {
170 $lang = $parser->getTargetLanguage();
171 $df = MediaWikiServices::getInstance()->getDateFormatterFactory()->get( $lang );
172
173 $date = trim( $date );
174
175 $pref = $parser->getOptions()->getDateFormat();
176
177 // Specify a different default date format other than the normal default
178 // if the user has 'default' for their setting
179 if ( $pref == 'default' && $defaultPref ) {
180 $pref = $defaultPref;
181 }
182
183 $date = $df->reformat( $pref, $date, [ 'match-whole' ] );
184 return $date;
185 }
186
192 public static function ns( $parser, $part1 = '' ) {
193 if ( intval( $part1 ) || $part1 == "0" ) {
194 $index = intval( $part1 );
195 } else {
196 $index = $parser->getContentLanguage()->getNsIndex( str_replace( ' ', '_', $part1 ) );
197 }
198 if ( $index !== false ) {
199 return $parser->getContentLanguage()->getFormattedNsText( $index );
200 } else {
201 return [ 'found' => false ];
202 }
203 }
204
210 public static function nse( $parser, $part1 = '' ) {
211 $ret = self::ns( $parser, $part1 );
212 if ( is_string( $ret ) ) {
213 $ret = wfUrlencode( str_replace( ' ', '_', $ret ) );
214 }
215 return $ret;
216 }
217
230 public static function urlencode( $parser, $s = '', $arg = null ) {
231 static $magicWords = null;
232 if ( $magicWords === null ) {
234 $parser->getMagicWordFactory()->newArray( [ 'url_path', 'url_query', 'url_wiki' ] );
235 }
236 switch ( $magicWords->matchStartToEnd( $arg ?? '' ) ) {
237 // Encode as though it's a wiki page, '_' for ' '.
238 case 'url_wiki':
239 $func = 'wfUrlencode';
240 $s = str_replace( ' ', '_', $s );
241 break;
242
243 // Encode for an HTTP Path, '%20' for ' '.
244 case 'url_path':
245 $func = 'rawurlencode';
246 break;
247
248 // Encode for HTTP query, '+' for ' '.
249 case 'url_query':
250 default:
251 $func = 'urlencode';
252 }
253 // See T105242, where the choice to kill markers and various
254 // other options were discussed.
255 return $func( $parser->killMarkers( $s ) );
256 }
257
263 public static function lcfirst( $parser, $s = '' ) {
264 return $parser->getContentLanguage()->lcfirst( $s );
265 }
266
272 public static function ucfirst( $parser, $s = '' ) {
273 return $parser->getContentLanguage()->ucfirst( $s );
274 }
275
281 public static function lc( $parser, $s = '' ) {
282 return $parser->markerSkipCallback( $s, [ $parser->getContentLanguage(), 'lc' ] );
283 }
284
290 public static function uc( $parser, $s = '' ) {
291 return $parser->markerSkipCallback( $s, [ $parser->getContentLanguage(), 'uc' ] );
292 }
293
300 public static function localurl( $parser, $s = '', $arg = null ) {
301 return self::urlFunction( 'getLocalURL', $s, $arg );
302 }
303
310 public static function localurle( $parser, $s = '', $arg = null ) {
311 $temp = self::urlFunction( 'getLocalURL', $s, $arg );
312 if ( !is_string( $temp ) ) {
313 return $temp;
314 } else {
315 return htmlspecialchars( $temp, ENT_COMPAT );
316 }
317 }
318
325 public static function fullurl( $parser, $s = '', $arg = null ) {
326 return self::urlFunction( 'getFullURL', $s, $arg );
327 }
328
335 public static function fullurle( $parser, $s = '', $arg = null ) {
336 $temp = self::urlFunction( 'getFullURL', $s, $arg );
337 if ( !is_string( $temp ) ) {
338 return $temp;
339 } else {
340 return htmlspecialchars( $temp, ENT_COMPAT );
341 }
342 }
343
350 public static function canonicalurl( $parser, $s = '', $arg = null ) {
351 return self::urlFunction( 'getCanonicalURL', $s, $arg );
352 }
353
360 public static function canonicalurle( $parser, $s = '', $arg = null ) {
361 $temp = self::urlFunction( 'getCanonicalURL', $s, $arg );
362 if ( !is_string( $temp ) ) {
363 return $temp;
364 } else {
365 return htmlspecialchars( $temp, ENT_COMPAT );
366 }
367 }
368
375 public static function urlFunction( $func, $s = '', $arg = null ) {
376 # Due to order of execution of a lot of bits, the values might be encoded
377 # before arriving here; if that's true, then the title can't be created
378 # and the variable will fail. If we can't get a decent title from the first
379 # attempt, url-decode and try for a second.
380 $title = Title::newFromText( $s ) ?? Title::newFromURL( urldecode( $s ) );
381 if ( $title !== null ) {
382 # Convert NS_MEDIA -> NS_FILE
383 if ( $title->inNamespace( NS_MEDIA ) ) {
384 $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
385 }
386 if ( $arg !== null ) {
387 $text = $title->$func( $arg );
388 } else {
389 $text = $title->$func();
390 }
391 return $text;
392 } else {
393 return [ 'found' => false ];
394 }
395 }
396
404 public static function formatnum( $parser, $num = '', $arg1 = '', $arg2 = '' ) {
405 static $magicWords = null;
406 if ( $magicWords === null ) {
407 $magicWords = $parser->getMagicWordFactory()->newArray( [
408 'rawsuffix',
409 'nocommafysuffix',
410 'lossless',
411 ] );
412 }
413
414 $modifiers = [ $magicWords->matchStartToEnd( $arg1 ), $magicWords->matchStartToEnd( $arg2 ) ];
415 $targetLanguage = $parser->getTargetLanguage();
416 if ( in_array( 'rawsuffix', $modifiers, true ) ) {
417 $func = [ $targetLanguage, 'parseFormattedNumber' ];
418 } else {
419 if ( in_array( 'nocommafysuffix', $modifiers, true ) ) {
420 $func = [ $targetLanguage, 'formatNumNoSeparators' ];
421 } else {
422 $func = [ $targetLanguage, 'formatNum' ];
423 $func = self::getLegacyFormatNum( $parser, $func );
424 }
425 if ( in_array( 'lossless', $modifiers, true ) ) {
426 $potentiallyLossyFunc = $func;
427 $func = static function ( $num ) use ( $targetLanguage, $potentiallyLossyFunc ) {
428 $formatted = $potentiallyLossyFunc( $num );
429 $parsed = $targetLanguage->parseFormattedNumber( $formatted );
430 if ( $num === $parsed ) {
431 return $formatted;
432 } else {
433 return (string)$num;
434 }
435 };
436 }
437 }
438 return $parser->markerSkipCallback( $num, $func );
439 }
440
447 private static function getLegacyFormatNum( $parser, $callback ) {
448 // For historic reasons, the formatNum parser function will
449 // take arguments which are not actually formatted numbers,
450 // which then trigger deprecation warnings in Language::formatNum*.
451 // Instead emit a tracking category instead to allow linting.
452 return static function ( $number ) use ( $parser, $callback ) {
453 $validNumberRe = '(-(?=[\d\.]))?(\d+|(?=\.\d))(\.\d*)?([Ee][-+]?\d+)?';
454 if (
455 !is_numeric( $number ) &&
456 $number !== (string)NAN &&
457 $number !== (string)INF &&
458 $number !== (string)-INF
459 ) {
460 $parser->addTrackingCategory( 'nonnumeric-formatnum' );
461 // Don't split on NAN/INF in the legacy case since they are
462 // likely to be found embedded inside non-numeric text.
463 return preg_replace_callback( "/{$validNumberRe}/", static function ( $m ) use ( $callback ) {
464 return $callback( $m[0] );
465 }, $number );
466 }
467 return $callback( $number );
468 };
469 }
470
477 public static function grammar( $parser, $case = '', $word = '' ) {
478 $word = $parser->killMarkers( $word );
479 return $parser->getTargetLanguage()->convertGrammar( $word, $case );
480 }
481
488 public static function gender( $parser, $username, ...$forms ) {
489 // Some shortcuts to avoid loading user data unnecessarily
490 if ( count( $forms ) === 0 ) {
491 return '';
492 } elseif ( count( $forms ) === 1 ) {
493 return $forms[0];
494 }
495
496 $username = trim( $username );
497
498 $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
499 $gender = $userOptionsLookup->getDefaultOption( 'gender' );
500
501 // allow prefix and normalize (e.g. "&#42;foo" -> "*foo" ).
502 $title = Title::newFromText( $username, NS_USER );
503
504 if ( $title && $title->inNamespace( NS_USER ) ) {
505 $username = $title->getText();
506 }
507
508 // check parameter, or use the ParserOptions if in interface message
509 $user = User::newFromName( $username );
510 $genderCache = MediaWikiServices::getInstance()->getGenderCache();
511 if ( $user ) {
512 $gender = $genderCache->getGenderOf( $user, __METHOD__ );
513 } elseif ( $username === '' && $parser->getOptions()->isMessage() ) {
514 $gender = $genderCache->getGenderOf( $parser->getOptions()->getUserIdentity(), __METHOD__ );
515 }
516 $ret = $parser->getTargetLanguage()->gender( $gender, $forms );
517 return $ret;
518 }
519
526 public static function plural( $parser, $text = '', ...$forms ) {
527 $text = $parser->getTargetLanguage()->parseFormattedNumber( $text );
528 settype( $text, ctype_digit( $text ) ? 'int' : 'float' );
529 // @phan-suppress-next-line PhanTypeMismatchArgument Phan does not handle settype
530 return $parser->getTargetLanguage()->convertPlural( $text, $forms );
531 }
532
533 public static function formal( Parser $parser, string ...$forms ): string {
534 $index = $parser->getTargetLanguage()->getFormalityIndex();
535 return $forms[$index] ?? $forms[0];
536 }
537
543 public static function bidi( $parser, $text = '' ) {
544 return $parser->getTargetLanguage()->embedBidi( $text );
545 }
546
556 public static function displaytitle( $parser, $text = '', $uarg = '' ) {
557 $restrictDisplayTitle = MediaWikiServices::getInstance()->getMainConfig()
559
560 static $magicWords = null;
561 if ( $magicWords === null ) {
562 $magicWords = $parser->getMagicWordFactory()->newArray(
563 [ 'displaytitle_noerror', 'displaytitle_noreplace' ] );
564 }
565 $arg = $magicWords->matchStartToEnd( $uarg );
566
567 // parse a limited subset of wiki markup (just the single quote items)
568 $text = $parser->doQuotes( $text );
569
570 // remove stripped text (e.g. the UNIQ-QINU stuff) that was generated by tag extensions/whatever
571 $text = $parser->killMarkers( $text );
572
573 // See T28547 for rationale for this processing.
574 // list of disallowed tags for DISPLAYTITLE
575 // these will be escaped even though they are allowed in normal wiki text
576 $bad = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'blockquote', 'ol', 'ul', 'li', 'hr',
577 'table', 'tr', 'th', 'td', 'dl', 'dd', 'caption', 'p', 'ruby', 'rb', 'rt', 'rtc', 'rp', 'br' ];
578
579 // disallow some styles that could be used to bypass $wgRestrictDisplayTitle
580 if ( $restrictDisplayTitle ) {
581 // This code is tested with the cases marked T28547 in
582 // parserTests.txt
583 $htmlTagsCallback = static function ( Attributes $attr ): Attributes {
584 $decoded = $attr->getValues();
585
586 if ( isset( $decoded['style'] ) ) {
587 // this is called later anyway, but we need it right now for the regexes below to be safe
588 // calling it twice doesn't hurt
589 $decoded['style'] = Sanitizer::checkCss( $decoded['style'] );
590
591 if ( preg_match( '/(display|user-select|visibility)\s*:/i', $decoded['style'] ) ) {
592 $decoded['style'] = '/* attempt to bypass $wgRestrictDisplayTitle */';
593 }
594 }
595
596 return new PlainAttributes( $decoded );
597 };
598 } else {
599 $htmlTagsCallback = null;
600 }
601
602 // only requested titles that normalize to the actual title are allowed through
603 // if $wgRestrictDisplayTitle is true (it is by default)
604 // mimic the escaping process that occurs in OutputPage::setPageTitle
605 $text = Sanitizer::removeSomeTags( $text, [
606 'attrCallback' => $htmlTagsCallback,
607 'removeTags' => $bad,
608 ] );
609 $title = Title::newFromText( Sanitizer::stripAllTags( $text ) );
610 // Decode entities in $text the same way that Title::newFromText does
611 $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
612
613 if ( !$restrictDisplayTitle ||
614 ( $title instanceof Title
615 && !$title->hasFragment()
616 && $title->equals( $parser->getTitle() ) )
617 ) {
618 $old = $parser->getOutput()->getPageProperty( 'displaytitle' );
619 if ( $old === null || $arg !== 'displaytitle_noreplace' ) {
620 $parser->getOutput()->setDisplayTitle( $text );
621 }
622 if ( $old !== null && $old !== $text && !$arg ) {
623
624 $converter = $parser->getTargetLanguageConverter();
625 return '<span class="error">' .
626 $parser->msg( 'duplicate-displaytitle',
627 // Message should be parsed, but these params should only be escaped.
628 $converter->markNoConversion( wfEscapeWikiText( $old ) ),
629 $converter->markNoConversion( wfEscapeWikiText( $filteredText ) )
630 )->text() .
631 '</span>';
632 } else {
633 return '';
634 }
635 } else {
636 $parser->getOutput()->addWarningMsg(
637 'restricted-displaytitle',
638 // Message should be parsed, but this param should only be escaped.
639 Message::plaintextParam( $filteredText )
640 );
641 $parser->addTrackingCategory( 'restricted-displaytitle-ignored' );
642 }
643 }
644
654 private static function matchAgainstMagicword(
655 MagicWordFactory $magicWordFactory, $magicword, $value
656 ) {
657 $value = trim( strval( $value ) );
658 if ( $value === '' ) {
659 return false;
660 }
661 $mwObject = $magicWordFactory->get( $magicword );
662 return $mwObject->matchStartToEnd( $value );
663 }
664
674 public static function formatRaw(
675 $num, $raw, $language, ?MagicWordFactory $magicWordFactory = null
676 ) {
677 if ( $raw !== null && $raw !== '' ) {
678 if ( !$magicWordFactory ) {
679 $magicWordFactory = MediaWikiServices::getInstance()->getMagicWordFactory();
680 }
681 if ( self::matchAgainstMagicword( $magicWordFactory, 'rawsuffix', $raw ) ) {
682 return (string)$num;
683 }
684 }
685 return $language->formatNum( $num );
686 }
687
693 public static function numberofpages( $parser, $raw = null ) {
694 return self::formatRaw( SiteStats::pages(), $raw, $parser->getTargetLanguage() );
695 }
696
702 public static function numberofusers( $parser, $raw = null ) {
703 return self::formatRaw( SiteStats::users(), $raw, $parser->getTargetLanguage() );
704 }
705
711 public static function numberofactiveusers( $parser, $raw = null ) {
712 return self::formatRaw( SiteStats::activeUsers(), $raw, $parser->getTargetLanguage() );
713 }
714
720 public static function numberofarticles( $parser, $raw = null ) {
721 return self::formatRaw( SiteStats::articles(), $raw, $parser->getTargetLanguage() );
722 }
723
729 public static function numberoffiles( $parser, $raw = null ) {
730 return self::formatRaw( SiteStats::images(), $raw, $parser->getTargetLanguage() );
731 }
732
738 public static function numberofadmins( $parser, $raw = null ) {
739 return self::formatRaw(
740 SiteStats::numberingroup( 'sysop' ),
741 $raw,
742 $parser->getTargetLanguage()
743 );
744 }
745
751 public static function numberofedits( $parser, $raw = null ) {
752 return self::formatRaw( SiteStats::edits(), $raw, $parser->getTargetLanguage() );
753 }
754
761 public static function pagesinnamespace( $parser, $namespace = '0', $raw = null ) {
762 return self::formatRaw(
763 SiteStats::pagesInNs( intval( $namespace ) ),
764 $raw,
765 $parser->getTargetLanguage()
766 );
767 }
768
775 public static function numberingroup( $parser, $name = '', $raw = null ) {
776 return self::formatRaw(
777 SiteStats::numberingroup( strtolower( $name ) ),
778 $raw,
779 $parser->getTargetLanguage()
780 );
781 }
782
791 private static function makeTitle( Parser $parser, ?string $t ) {
792 if ( $t === null ) {
793 // For consistency with magic variable forms
794 $title = $parser->getTitle();
795 } else {
796 $title = Title::newFromText( $t );
797 }
798 return $title;
799 }
800
809 public static function namespace( $parser, $title = null ) {
810 $t = self::makeTitle( $parser, $title );
811 if ( $t === null ) {
812 return '';
813 }
814 return str_replace( '_', ' ', $t->getNsText() );
815 }
816
822 public static function namespacee( $parser, $title = null ) {
823 $t = self::makeTitle( $parser, $title );
824 if ( $t === null ) {
825 return '';
826 }
827 return wfUrlencode( $t->getNsText() );
828 }
829
835 public static function namespacenumber( $parser, $title = null ) {
836 $t = self::makeTitle( $parser, $title );
837 if ( $t === null ) {
838 return '';
839 }
840 return (string)$t->getNamespace();
841 }
842
848 public static function talkspace( $parser, $title = null ) {
849 $t = self::makeTitle( $parser, $title );
850 if ( $t === null || !$t->canHaveTalkPage() ) {
851 return '';
852 }
853 return str_replace( '_', ' ', $t->getTalkNsText() );
854 }
855
861 public static function talkspacee( $parser, $title = null ) {
862 $t = self::makeTitle( $parser, $title );
863 if ( $t === null || !$t->canHaveTalkPage() ) {
864 return '';
865 }
866 return wfUrlencode( $t->getTalkNsText() );
867 }
868
874 public static function subjectspace( $parser, $title = null ) {
875 $t = self::makeTitle( $parser, $title );
876 if ( $t === null ) {
877 return '';
878 }
879 return str_replace( '_', ' ', $t->getSubjectNsText() );
880 }
881
887 public static function subjectspacee( $parser, $title = null ) {
888 $t = self::makeTitle( $parser, $title );
889 if ( $t === null ) {
890 return '';
891 }
892 return wfUrlencode( $t->getSubjectNsText() );
893 }
894
902 public static function pagename( $parser, $title = null ) {
903 $t = self::makeTitle( $parser, $title );
904 if ( $t === null ) {
905 return '';
906 }
907 return wfEscapeWikiText( $t->getText() );
908 }
909
915 public static function pagenamee( $parser, $title = null ) {
916 $t = self::makeTitle( $parser, $title );
917 if ( $t === null ) {
918 return '';
919 }
920 return wfEscapeWikiText( $t->getPartialURL() );
921 }
922
928 public static function fullpagename( $parser, $title = null ) {
929 $t = self::makeTitle( $parser, $title );
930 if ( $t === null ) {
931 return '';
932 }
933 return wfEscapeWikiText( $t->getPrefixedText() );
934 }
935
941 public static function fullpagenamee( $parser, $title = null ) {
942 $t = self::makeTitle( $parser, $title );
943 if ( $t === null ) {
944 return '';
945 }
946 return wfEscapeWikiText( $t->getPrefixedURL() );
947 }
948
954 public static function subpagename( $parser, $title = null ) {
955 $t = self::makeTitle( $parser, $title );
956 if ( $t === null ) {
957 return '';
958 }
959 return wfEscapeWikiText( $t->getSubpageText() );
960 }
961
967 public static function subpagenamee( $parser, $title = null ) {
968 $t = self::makeTitle( $parser, $title );
969 if ( $t === null ) {
970 return '';
971 }
972 return wfEscapeWikiText( $t->getSubpageUrlForm() );
973 }
974
980 public static function rootpagename( $parser, $title = null ) {
981 $t = self::makeTitle( $parser, $title );
982 if ( $t === null ) {
983 return '';
984 }
985 return wfEscapeWikiText( $t->getRootText() );
986 }
987
993 public static function rootpagenamee( $parser, $title = null ) {
994 $t = self::makeTitle( $parser, $title );
995 if ( $t === null ) {
996 return '';
997 }
998 return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getRootText() ) ) );
999 }
1000
1006 public static function basepagename( $parser, $title = null ) {
1007 $t = self::makeTitle( $parser, $title );
1008 if ( $t === null ) {
1009 return '';
1010 }
1011 return wfEscapeWikiText( $t->getBaseText() );
1012 }
1013
1019 public static function basepagenamee( $parser, $title = null ) {
1020 $t = self::makeTitle( $parser, $title );
1021 if ( $t === null ) {
1022 return '';
1023 }
1024 return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getBaseText() ) ) );
1025 }
1026
1032 public static function talkpagename( $parser, $title = null ) {
1033 $t = self::makeTitle( $parser, $title );
1034 if ( $t === null || !$t->canHaveTalkPage() ) {
1035 return '';
1036 }
1037 return wfEscapeWikiText( $t->getTalkPage()->getPrefixedText() );
1038 }
1039
1045 public static function talkpagenamee( $parser, $title = null ) {
1046 $t = self::makeTitle( $parser, $title );
1047 if ( $t === null || !$t->canHaveTalkPage() ) {
1048 return '';
1049 }
1050 return wfEscapeWikiText( $t->getTalkPage()->getPrefixedURL() );
1051 }
1052
1058 public static function subjectpagename( $parser, $title = null ) {
1059 $t = self::makeTitle( $parser, $title );
1060 if ( $t === null ) {
1061 return '';
1062 }
1063 return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedText() );
1064 }
1065
1071 public static function subjectpagenamee( $parser, $title = null ) {
1072 $t = self::makeTitle( $parser, $title );
1073 if ( $t === null ) {
1074 return '';
1075 }
1076 return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedURL() );
1077 }
1078
1089 public static function pagesincategory( $parser, $name = '', $arg1 = '', $arg2 = '' ) {
1090 static $magicWords = null;
1091 if ( $magicWords === null ) {
1092 $magicWords = $parser->getMagicWordFactory()->newArray( [
1093 'pagesincategory_all',
1094 'pagesincategory_pages',
1095 'pagesincategory_subcats',
1096 'pagesincategory_files'
1097 ] );
1098 }
1099 static $cache = [];
1100
1101 // split the given option to its variable
1102 if ( self::matchAgainstMagicword( $parser->getMagicWordFactory(), 'rawsuffix', $arg1 ) ) {
1103 // {{pagesincategory:|raw[|type]}}
1104 $raw = $arg1;
1105 $type = $magicWords->matchStartToEnd( $arg2 );
1106 } else {
1107 // {{pagesincategory:[|type[|raw]]}}
1108 $type = $magicWords->matchStartToEnd( $arg1 );
1109 $raw = $arg2;
1110 }
1111 if ( !$type ) { // backward compatibility
1112 $type = 'pagesincategory_all';
1113 }
1114
1115 $title = Title::makeTitleSafe( NS_CATEGORY, $name );
1116 if ( !$title ) { # invalid title
1117 return self::formatRaw( 0, $raw, $parser->getTargetLanguage() );
1118 }
1119 $languageConverter = MediaWikiServices::getInstance()
1120 ->getLanguageConverterFactory()
1121 ->getLanguageConverter( $parser->getContentLanguage() );
1122 $languageConverter->findVariantLink( $name, $title, true );
1123
1124 // Normalize name for cache
1125 $name = $title->getDBkey();
1126
1127 if ( !isset( $cache[$name] ) ) {
1128 $category = Category::newFromTitle( $title );
1129
1130 $allCount = $subcatCount = $fileCount = $pageCount = 0;
1131 if ( $parser->incrementExpensiveFunctionCount() ) {
1132 $allCount = $category->getMemberCount();
1133 $subcatCount = $category->getSubcatCount();
1134 $fileCount = $category->getFileCount();
1135 $pageCount = $category->getPageCount( Category::COUNT_CONTENT_PAGES );
1136 }
1137 $cache[$name]['pagesincategory_all'] = $allCount;
1138 $cache[$name]['pagesincategory_pages'] = $pageCount;
1139 $cache[$name]['pagesincategory_subcats'] = $subcatCount;
1140 $cache[$name]['pagesincategory_files'] = $fileCount;
1141 }
1142
1143 $count = $cache[$name][$type];
1144 return self::formatRaw( $count, $raw, $parser->getTargetLanguage() );
1145 }
1146
1156 public static function pagesize( $parser, $page = '', $raw = null ) {
1157 $title = Title::newFromText( $page );
1158
1159 if ( !is_object( $title ) || $title->isExternal() ) {
1160 return self::formatRaw( 0, $raw, $parser->getTargetLanguage() );
1161 }
1162
1163 // fetch revision from cache/database and return the value
1164 $rev = self::getCachedRevisionObject( $parser, $title, ParserOutputFlags::VARY_REVISION_SHA1 );
1165 $length = $rev ? $rev->getSize() : 0;
1166 if ( $length === null ) {
1167 // We've had bugs where rev_len was not being recorded for empty pages, see T135414
1168 $length = 0;
1169 }
1170 return self::formatRaw( $length, $raw, $parser->getTargetLanguage() );
1171 }
1172
1185 public static function protectionlevel( $parser, $type = '', $title = '' ) {
1186 $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
1187 $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
1188 if ( $restrictionStore->areRestrictionsLoaded( $titleObject ) || $parser->incrementExpensiveFunctionCount() ) {
1189 $restrictions = $restrictionStore->getRestrictions( $titleObject, strtolower( $type ) );
1190 # RestrictionStore::getRestrictions returns an array, its possible it may have
1191 # multiple values in the future
1192 return implode( ',', $restrictions );
1193 }
1194 return '';
1195 }
1196
1209 public static function protectionexpiry( $parser, $type = '', $title = '' ) {
1210 $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
1211 $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
1212 if ( $restrictionStore->areRestrictionsLoaded( $titleObject ) || $parser->incrementExpensiveFunctionCount() ) {
1213 // getRestrictionExpiry() returns null on invalid type; trying to
1214 // match protectionlevel() function that returns empty string instead
1215 return $restrictionStore->getRestrictionExpiry( $titleObject, strtolower( $type ) ) ?? '';
1216 }
1217 return '';
1218 }
1219
1228 public static function language( $parser, $code = '', $inLanguage = '' ) {
1229 if ( $code === '' ) {
1230 $code = $parser->getTargetLanguage()->getCode();
1231 }
1232 if ( $inLanguage === '' ) {
1233 $inLanguage = LanguageNameUtils::AUTONYMS;
1234 }
1236 ->getLanguageNameUtils()
1237 ->getLanguageName( $code, $inLanguage );
1238 return $lang !== '' ? $lang : LanguageCode::bcp47( $code );
1239 }
1240
1252 public static function dir( Parser $parser, string $code = '', string $arg = '' ): string {
1253 static $magicWords = null;
1254 $languageFactory = MediaWikiServices::getInstance()->getLanguageFactory();
1255
1256 if ( $code === '' ) {
1257 $lang = $parser->getTargetLanguage();
1258 } else {
1259 if ( $arg !== '' ) {
1260 if ( $magicWords === null ) {
1261 $magicWords = $parser->getMagicWordFactory()->newArray( [ 'language_option_bcp47' ] );
1262 }
1263 if ( $magicWords->matchStartToEnd( $arg ) === 'language_option_bcp47' ) {
1264 // Prefer the BCP-47 interpretation of this code.
1265 $code = new Bcp47CodeValue( $code );
1266 }
1267 }
1268 try {
1269 $lang = $languageFactory->getLanguage( $code );
1270 } catch ( InvalidArgumentException ) {
1271 $parser->addTrackingCategory( 'bad-language-code-category' );
1272 return 'ltr';
1273 }
1274 }
1275 return $lang->getDir();
1276 }
1277
1286 public static function bcp47( Parser $parser, string $code = '' ): string {
1287 if ( $code === '' ) {
1288 return $parser->getTargetLanguage()->toBcp47Code();
1289 } else {
1290 return LanguageCode::bcp47( $code );
1291 }
1292 }
1293
1303 public static function pad(
1304 $parser, $string, $length, $padding = '0', $direction = STR_PAD_RIGHT
1305 ) {
1306 $padding = $parser->killMarkers( $padding );
1307 $lengthOfPadding = mb_strlen( $padding );
1308 if ( $lengthOfPadding == 0 ) {
1309 return $string;
1310 }
1311
1312 # The remaining length to add counts down to 0 as padding is added
1313 $length = min( (int)$length, 500 ) - mb_strlen( $string );
1314 if ( $length <= 0 ) {
1315 // Nothing to add
1316 return $string;
1317 }
1318
1319 # $finalPadding is just $padding repeated enough times so that
1320 # mb_strlen( $string ) + mb_strlen( $finalPadding ) == $length
1321 $finalPadding = '';
1322 while ( $length > 0 ) {
1323 # If $length < $lengthofPadding, truncate $padding so we get the
1324 # exact length desired.
1325 $finalPadding .= mb_substr( $padding, 0, $length );
1326 $length -= $lengthOfPadding;
1327 }
1328
1329 if ( $direction == STR_PAD_LEFT ) {
1330 return $finalPadding . $string;
1331 } else {
1332 return $string . $finalPadding;
1333 }
1334 }
1335
1343 public static function padleft( $parser, $string = '', $length = '0', $padding = '0' ) {
1344 return self::pad( $parser, $string, $length, $padding, STR_PAD_LEFT );
1345 }
1346
1354 public static function padright( $parser, $string = '', $length = '0', $padding = '0' ) {
1355 return self::pad( $parser, $string, $length, $padding );
1356 }
1357
1363 public static function anchorencode( $parser, $text ) {
1364 $text = $parser->killMarkers( $text );
1365 $section = substr( $parser->guessSectionNameFromWikiText( $text ), 1 );
1366 return Sanitizer::safeEncodeAttribute( $section );
1367 }
1368
1374 public static function special( $parser, $text ) {
1375 [ $page, $subpage ] = MediaWikiServices::getInstance()->getSpecialPageFactory()->
1376 resolveAlias( $text );
1377 if ( $page ) {
1378 $title = SpecialPage::getTitleFor( $page, $subpage );
1379 return $title->getPrefixedText();
1380 } else {
1381 // unknown special page, just use the given text as its title, if at all possible
1382 $title = Title::makeTitleSafe( NS_SPECIAL, $text );
1383 return $title ? $title->getPrefixedText() : self::special( $parser, 'Badtitle' );
1384 }
1385 }
1386
1392 public static function speciale( $parser, $text ) {
1393 return wfUrlencode( str_replace( ' ', '_', self::special( $parser, $text ) ) );
1394 }
1395
1404 public static function defaultsort( $parser, $text, $uarg = '' ) {
1405 static $magicWords = null;
1406 if ( $magicWords === null ) {
1407 $magicWords = $parser->getMagicWordFactory()->newArray(
1408 [ 'defaultsort_noerror', 'defaultsort_noreplace' ] );
1409 }
1410 $arg = $magicWords->matchStartToEnd( $uarg );
1411
1412 $text = trim( $text );
1413 if ( $text === '' ) {
1414 return '';
1415 }
1416 $old = $parser->getOutput()->getPageProperty( 'defaultsort' );
1417 if ( $old === null || $arg !== 'defaultsort_noreplace' ) {
1418 $parser->getOutput()->setPageProperty( 'defaultsort', $text );
1419 }
1420
1421 if ( $old === null || $old == $text || $arg ) {
1422 return '';
1423 } else {
1424 $converter = $parser->getTargetLanguageConverter();
1425 return '<span class="error">' .
1426 $parser->msg( 'duplicate-defaultsort',
1427 // Message should be parsed, but these params should only be escaped.
1428 $converter->markNoConversion( wfEscapeWikiText( $old ) ),
1429 $converter->markNoConversion( wfEscapeWikiText( $text ) )
1430 )->text() .
1431 '</span>';
1432 }
1433 }
1434
1446 public static function filepath( $parser, $name = '', $argA = '', $argB = '' ) {
1447 $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $name );
1448
1449 if ( $argA == 'nowiki' ) {
1450 // {{filepath: | option [| size] }}
1451 $isNowiki = true;
1452 $parsedWidthParam = $parser->parseWidthParam( $argB );
1453 } else {
1454 // {{filepath: [| size [|option]] }}
1455 $parsedWidthParam = $parser->parseWidthParam( $argA );
1456 $isNowiki = ( $argB == 'nowiki' );
1457 }
1458
1459 if ( $file ) {
1460 $url = $file->getFullUrl();
1461
1462 // If a size is requested...
1463 if ( count( $parsedWidthParam ) ) {
1464 $mto = $file->transform( $parsedWidthParam );
1465 // ... and we can
1466 if ( $mto && !$mto->isError() ) {
1467 // ... change the URL to point to a thumbnail.
1468 $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
1469 $url = $urlUtils->expand( $mto->getUrl(), PROTO_RELATIVE ) ?? false;
1470 }
1471 }
1472 if ( $isNowiki ) {
1473 return [ $url, 'nowiki' => true ];
1474 }
1475 return $url;
1476 } else {
1477 return '';
1478 }
1479 }
1480
1488 public static function tagObj( $parser, $frame, $args ) {
1489 if ( !count( $args ) ) {
1490 return '';
1491 }
1492 $tagName = strtolower( trim( $parser->killMarkers(
1493 $frame->expand( array_shift( $args ) )
1494 ) ) );
1495 $processNowiki = $parser->tagNeedsNowikiStrippedInTagPF( $tagName ) ? PPFrame::PROCESS_NOWIKI : 0;
1496
1497 if ( count( $args ) ) {
1498 // With Parsoid Fragment support, $processNoWiki flag
1499 // isn't actually required as a ::expand() flag, but it
1500 // doesn't do any harm.
1501 $inner = $frame->expand( array_shift( $args ), $processNowiki );
1502 if ( $processNowiki ) {
1503 // This is the T299103 workaround for <syntaxhighlight>,
1504 // and reproduces the code in SyntaxHighlight::parserHook.
1505 // The Parsoid extension API (SyntaxHighlight::sourceToDom)
1506 // doesn't (yet) know about strip state, and so can't do
1507 // this itself.
1508 $inner = $parser->getStripState()->unstripNoWiki( $inner );
1509 }
1510 } else {
1511 $inner = null;
1512 }
1513
1514 $attributes = [];
1515 foreach ( $args as $arg ) {
1516 $bits = $arg->splitArg();
1517 if ( strval( $bits['index'] ) === '' ) {
1518 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
1519 $value = trim( $frame->expand( $bits['value'] ) );
1520 if ( preg_match( '/^(?:["\'](.+)["\']|""|\'\')$/s', $value, $m ) ) {
1521 $value = $m[1] ?? '';
1522 }
1523 $attributes[$name] = $value;
1524 }
1525 }
1526
1527 $stripList = $parser->getStripList();
1528 if ( !in_array( $tagName, $stripList ) ) {
1529 // we can't handle this tag (at least not now), so just re-emit it as an ordinary tag
1530 $attrText = '';
1531 foreach ( $attributes as $name => $value ) {
1532 $attrText .= ' ' . htmlspecialchars( $name ) .
1533 '="' . htmlspecialchars( $parser->killMarkers( $value ), ENT_COMPAT ) . '"';
1534 }
1535 if ( $inner === null ) {
1536 return "<$tagName$attrText/>";
1537 }
1538 return "<$tagName$attrText>$inner</$tagName>";
1539 }
1540
1541 $params = [
1542 'name' => $tagName,
1543 'inner' => $inner,
1544 'attributes' => $attributes,
1545 'close' => "</$tagName>",
1546 ];
1547 return $parser->extensionSubstitution( $params, $frame );
1548 }
1549
1563 private static function getCachedRevisionObject( $parser, $title, $vary ) {
1564 if ( !$title ) {
1565 return null;
1566 }
1567
1568 $revisionRecord = null;
1569
1570 $isSelfReferential = $title->equals( $parser->getTitle() );
1571 if ( $isSelfReferential ) {
1572 // Revision is for the same title that is currently being parsed. Only use the last
1573 // saved revision, regardless of Parser::getRevisionId() or fake revision injection
1574 // callbacks against the current title.
1575
1576 // FIXME (T318278): the above is the intention, but doesn't
1577 // describe the actual current behavior of this code, since
1578 // ->isCurrent() for the last saved revision will return
1579 // false so we're going to fall through and end up calling
1580 // ->getCurrentRevisionRecordOfTitle().
1581 $parserRevisionRecord = $parser->getRevisionRecordObject();
1582 if ( $parserRevisionRecord && $parserRevisionRecord->isCurrent() ) {
1583 $revisionRecord = $parserRevisionRecord;
1584 }
1585 }
1586
1587 $parserOutput = $parser->getOutput();
1588 if ( !$revisionRecord ) {
1589 if (
1590 !$parser->isCurrentRevisionOfTitleCached( $title ) &&
1592 ) {
1593 return null; // not allowed
1594 }
1595 // Get the current revision, ignoring Parser::getRevisionId() being null/old
1596 $revisionRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
1597 if ( !$revisionRecord ) {
1598 // Convert `false` error return to `null`
1599 $revisionRecord = null;
1600 }
1601 // Register dependency in templatelinks
1602 $parserOutput->addTemplate(
1603 $title,
1604 $revisionRecord ? $revisionRecord->getPageId() : 0,
1605 $revisionRecord ? $revisionRecord->getId() : 0
1606 );
1607 }
1608
1609 if ( $isSelfReferential ) {
1610 wfDebug( __METHOD__ . ": used current revision, setting {$vary->value}" );
1611 // Upon page save, the result of the parser function using this might change
1612 $parserOutput->setOutputFlag( $vary );
1613 if ( $vary === ParserOutputFlags::VARY_REVISION_SHA1 && $revisionRecord ) {
1614 try {
1615 $sha1 = $revisionRecord->getSha1();
1616 } catch ( RevisionAccessException ) {
1617 $sha1 = null;
1618 }
1619 $parserOutput->setRevisionUsedSha1Base36( $sha1 );
1620 }
1621 }
1622
1623 return $revisionRecord;
1624 }
1625
1633 public static function pageid( $parser, $title = null ) {
1634 $t = self::makeTitle( $parser, $title );
1635 if ( !$t ) {
1636 return '';
1637 } elseif ( !$t->canExist() || $t->isExternal() ) {
1638 return 0; // e.g. special page or interwiki link
1639 }
1640
1641 $parserOutput = $parser->getOutput();
1642
1643 if ( $t->equals( $parser->getTitle() ) ) {
1644 // Revision is for the same title that is currently being parsed.
1645 // Use the title from Parser in case a new page ID was injected into it.
1646 $parserOutput->setOutputFlag( ParserOutputFlags::VARY_PAGE_ID );
1647 $id = $parser->getTitle()->getArticleID();
1648 if ( $id ) {
1649 $parserOutput->setSpeculativePageIdUsed( $id );
1650 }
1651
1652 return $id;
1653 }
1654
1655 // Check the link cache for the title
1656 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1657 $pdbk = $t->getPrefixedDBkey();
1658 $id = $linkCache->getGoodLinkID( $pdbk );
1659 if ( $id != 0 || $linkCache->isBadLink( $pdbk ) ) {
1660 $parserOutput->addLink( $t, $id );
1661
1662 return $id;
1663 }
1664
1665 // We need to load it from the DB, so mark expensive
1666 if ( $parser->incrementExpensiveFunctionCount() ) {
1667 $id = $t->getArticleID();
1668 $parserOutput->addLink( $t, $id );
1669
1670 return $id;
1671 }
1672
1673 return null;
1674 }
1675
1683 public static function revisionid( $parser, $title = null ) {
1684 $t = self::makeTitle( $parser, $title );
1685 if ( $t === null || $t->isExternal() ) {
1686 return '';
1687 }
1688
1689 $services = MediaWikiServices::getInstance();
1690 if (
1691 $t->equals( $parser->getTitle() ) &&
1692 $services->getMainConfig()->get( MainConfigNames::MiserMode ) &&
1693 !$parser->getOptions()->getInterfaceMessage() &&
1694 // @TODO: disallow this word on all namespaces (T235957)
1695 $services->getNamespaceInfo()->isSubject( $t->getNamespace() )
1696 ) {
1697 // Use a stub result instead of the actual revision ID in order to avoid
1698 // double parses on page save but still allow preview detection (T137900)
1699 if ( $parser->getRevisionId() || $parser->getOptions()->getSpeculativeRevId() ) {
1700 return '-';
1701 } else {
1702 $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_EXISTS );
1703 return '';
1704 }
1705 }
1706 // Fetch revision from cache/database and return the value.
1707 // Inform the edit saving system that getting the canonical output
1708 // after revision insertion requires a parse that used that exact
1709 // revision ID.
1710 if ( $t->equals( $parser->getTitle() ) && $title === null ) {
1711 // special handling for no-arg case: use speculative rev id
1712 // for current page.
1713 $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_ID );
1714 $id = $parser->getRevisionId();
1715 if ( $id === 0 ) {
1716 $rev = $parser->getRevisionRecordObject();
1717 if ( $rev ) {
1718 $id = $rev->getId();
1719 }
1720 }
1721 if ( !$id ) {
1722 $id = $parser->getOptions()->getSpeculativeRevId();
1723 if ( $id ) {
1724 $parser->getOutput()->setSpeculativeRevIdUsed( $id );
1725 }
1726 }
1727 return (string)$id;
1728 }
1729 $rev = self::getCachedRevisionObject( $parser, $t, ParserOutputFlags::VARY_REVISION_ID );
1730 return $rev ? $rev->getId() : '';
1731 }
1732
1733 private static function getRevisionTimestampSubstring(
1734 Parser $parser,
1735 Title $title,
1736 int $start,
1737 int $len,
1738 int $mtts
1739 ): string {
1740 // If fetching the revision timestamp of the current page, substitute the
1741 // speculative timestamp to be used when this revision is saved. This
1742 // avoids having to invalidate the cache immediately by assuming the "last
1743 // saved revision" will in fact be this one.
1744 // Don't do this for interface messages (eg, edit notices) however; in that
1745 // case fall through and use the actual timestamp of the last saved revision.
1746 if ( $title->equals( $parser->getTitle() ) && !$parser->getOptions()->getInterfaceMessage() ) {
1747 // Get the timezone-adjusted timestamp to be used for this revision
1748 $resNow = substr( $parser->getRevisionTimestamp(), $start, $len );
1749 // Possibly set vary-revision if there is not yet an associated revision
1750 if ( !$parser->getRevisionRecordObject() ) {
1751 // Get the timezone-adjusted timestamp $mtts seconds in the future.
1752 // This future is relative to the current time and not that of the
1753 // parser options. The rendered timestamp can be compared to that
1754 // of the timestamp specified by the parser options.
1755 $resThen = substr(
1756 $parser->getContentLanguage()->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
1757 $start,
1758 $len
1759 );
1760
1761 if ( $resNow !== $resThen ) {
1762 // Inform the edit saving system that getting the canonical output after
1763 // revision insertion requires a parse that used an actual revision timestamp
1764 $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_TIMESTAMP );
1765 }
1766 }
1767
1768 return $resNow;
1769 } else {
1770 $rev = self::getCachedRevisionObject( $parser, $title, ParserOutputFlags::VARY_REVISION_TIMESTAMP );
1771 if ( !$rev ) {
1772 return '';
1773 }
1774 $resNow = substr(
1775 $parser->getContentLanguage()->userAdjust( $rev->getTimestamp(), '' ), $start, $len
1776 );
1777 return $resNow;
1778 }
1779 }
1780
1788 public static function revisionday( $parser, $title = null ) {
1789 $t = self::makeTitle( $parser, $title );
1790 if ( $t === null || $t->isExternal() ) {
1791 return '';
1792 }
1793 return strval( (int)self::getRevisionTimestampSubstring(
1794 $parser, $t, 6, 2, self::MAX_TTS
1795 ) );
1796 }
1797
1805 public static function revisionday2( $parser, $title = null ) {
1806 $t = self::makeTitle( $parser, $title );
1807 if ( $t === null || $t->isExternal() ) {
1808 return '';
1809 }
1810 return self::getRevisionTimestampSubstring(
1811 $parser, $t, 6, 2, self::MAX_TTS
1812 );
1813 }
1814
1822 public static function revisionmonth( $parser, $title = null ) {
1823 $t = self::makeTitle( $parser, $title );
1824 if ( $t === null || $t->isExternal() ) {
1825 return '';
1826 }
1827 return self::getRevisionTimestampSubstring(
1828 $parser, $t, 4, 2, self::MAX_TTS
1829 );
1830 }
1831
1839 public static function revisionmonth1( $parser, $title = null ) {
1840 $t = self::makeTitle( $parser, $title );
1841 if ( $t === null || $t->isExternal() ) {
1842 return '';
1843 }
1844 return strval( (int)self::getRevisionTimestampSubstring(
1845 $parser, $t, 4, 2, self::MAX_TTS
1846 ) );
1847 }
1848
1856 public static function revisionyear( $parser, $title = null ) {
1857 $t = self::makeTitle( $parser, $title );
1858 if ( $t === null || $t->isExternal() ) {
1859 return '';
1860 }
1861 return self::getRevisionTimestampSubstring(
1862 $parser, $t, 0, 4, self::MAX_TTS
1863 );
1864 }
1865
1873 public static function revisiontimestamp( $parser, $title = null ) {
1874 $t = self::makeTitle( $parser, $title );
1875 if ( $t === null || $t->isExternal() ) {
1876 return '';
1877 }
1878 return self::getRevisionTimestampSubstring(
1879 $parser, $t, 0, 14, self::MAX_TTS
1880 );
1881 }
1882
1890 public static function revisionuser( $parser, $title = null ) {
1891 $t = self::makeTitle( $parser, $title );
1892 if ( $t === null || $t->isExternal() ) {
1893 return '';
1894 }
1895 // VARY_USER informs the edit saving system that getting the canonical
1896 // output after revision insertion requires a parse that used the
1897 // actual user ID.
1898 if ( $t->equals( $parser->getTitle() ) ) {
1899 // Fall back to Parser's "revision user" for the current title
1900 $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_USER );
1901 // Note that getRevisionUser() can return null; we need to
1902 // be sure to cast this to (an empty) string, since returning
1903 // null means "magic variable not handled".
1904 return (string)$parser->getRevisionUser();
1905 }
1906 // Fetch revision from cache/database and return the value.
1907 $rev = self::getCachedRevisionObject( $parser, $t, ParserOutputFlags::VARY_USER );
1908 $user = ( $rev !== null ) ? $rev->getUser() : null;
1909 return $user ? $user->getName() : '';
1910 }
1911
1924 public static function cascadingsources( $parser, $title = '' ) {
1925 $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
1926 $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
1927 if ( $restrictionStore->areCascadeProtectionSourcesLoaded( $titleObject )
1929 ) {
1930 $names = [];
1931 $sources = $restrictionStore->getCascadeProtectionSources( $titleObject );
1932 $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
1933 foreach ( $sources[0] as $sourcePageIdentity ) {
1934 $names[] = $titleFormatter->getPrefixedText( $sourcePageIdentity );
1935 }
1936 return implode( '|', $names );
1937 }
1938 return '';
1939 }
1940
1948 public static function contentmodel( Parser $parser, ?string $format = null, ?string $title = null ) {
1949 static $magicWords = null;
1950 if ( $magicWords === null ) {
1951 $magicWords = $parser->getMagicWordFactory()->newArray( [
1952 'contentmodel_canonical',
1953 'contentmodel_local',
1954 ] );
1955 }
1956
1957 $formatType = $format === null ? 'contentmodel_local' :
1958 $magicWords->matchStartToEnd( $format );
1959
1960 $t = self::makeTitle( $parser, $title );
1961 if ( $t === null ) {
1962 return '';
1963 }
1964
1965 if ( !$parser->incrementExpensiveFunctionCount() ) {
1966 return '';
1967 }
1968
1969 $contentModel = $t->getContentModel();
1970 if ( $formatType === 'contentmodel_canonical' ) {
1971 return wfEscapeWikiText( $contentModel );
1972 } elseif ( $formatType === 'contentmodel_local' ) {
1973 $localizedContentModel = ContentHandler::getLocalizedName( $contentModel, $parser->getTargetLanguage() );
1974 return wfEscapeWikiText( $localizedContentModel );
1975 } else {
1976 // Unknown format option
1977 return '';
1978 }
1979 }
1980
1988 public static function interwikilink( $parser, $prefix = '', $title = '', $linkText = null ) {
1989 $services = MediaWikiServices::getInstance();
1990 if (
1991 $prefix !== '' &&
1992 $services->getInterwikiLookup()->isValidInterwiki( $prefix )
1993 ) {
1994
1995 $target = self::getTitleValueSafe( $title, $prefix );
1996
1997 if ( $target !== null ) {
1998 if ( $linkText !== null ) {
1999 $linkText = Parser::stripOuterParagraph(
2000 # FIXME T382287: when using Parsoid this may leave
2001 # strip markers behind for embedded extension tags.
2002 $parser->recursiveTagParseFully( $linkText )
2003 );
2004 }
2005 $parser->getOutput()->addInterwikiLink( $target );
2006 return [
2007 'text' => Linker::link( $target, $linkText ),
2008 'isHTML' => true,
2009 ];
2010 }
2011 }
2012 // Invalid interwiki link, render as plain text
2013 return [ 'found' => false ];
2014 }
2015
2023 public static function interlanguagelink( $parser, $prefix = '', $title = '', $linkText = null ) {
2024 $services = MediaWikiServices::getInstance();
2025 $extraInterlanguageLinkPrefixes = $services->getMainConfig()->get(
2026 MainConfigNames::ExtraInterlanguageLinkPrefixes
2027 );
2028 if (
2029 $prefix !== '' &&
2030 $services->getInterwikiLookup()->isValidInterwiki( $prefix ) &&
2031 (
2032 $services->getLanguageNameUtils()->getLanguageName(
2033 $prefix, LanguageNameUtils::AUTONYMS, LanguageNameUtils::DEFINED
2034 ) || in_array( $prefix, $extraInterlanguageLinkPrefixes, true )
2035 )
2036 ) {
2037 // $linkText is ignored for language links, but fragment is kept
2038 $target = self::getTitleValueSafe( $title, $prefix );
2039
2040 if ( $target !== null ) {
2041 $parser->getOutput()->addLanguageLink( $target );
2042 return '';
2043 }
2044 }
2045 // Invalid language link, render as plain text
2046 return [ 'found' => false ];
2047 }
2048
2064 private static function getTitleValueSafe( $title, $prefix ): ?TitleValue {
2065 [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
2066
2067 try {
2068 return new TitleValue( NS_MAIN, $title, $frag, $prefix );
2069 } catch ( InvalidArgumentException ) {
2070 return null;
2071 }
2072 }
2073}
2074
2076class_alias( CoreParserFunctions::class, 'CoreParserFunctions' );
const NS_USER
Definition Defines.php:53
const NS_FILE
Definition Defines.php:57
const NS_MAIN
Definition Defines.php:51
const NS_SPECIAL
Definition Defines.php:40
const NS_MEDIA
Definition Defines.php:39
const PROTO_RELATIVE
Definition Defines.php:219
const NS_CATEGORY
Definition Defines.php:65
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$magicWords
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
Category objects are immutable, strictly speaking.
Definition Category.php:28
A class for passing options to services.
Base class for content handling.
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
Methods for dealing with language codes.
Base class for language-specific code.
Definition Language.php:69
Some internal bits split of from Skin.php.
Definition Linker.php:47
A class containing constants representing the names of configuration variables.
const AllowSlowParserFunctions
Name constant for the AllowSlowParserFunctions setting, for use with Config::get()
const AllowDisplayTitle
Name constant for the AllowDisplayTitle setting, for use with Config::get()
const RestrictDisplayTitle
Name constant for the RestrictDisplayTitle setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
static plaintextParam( $plaintext)
Definition Message.php:1341
Various core parser functions, registered in every Parser.
static pagesize( $parser, $page='', $raw=null)
Return the size of the given page, or 0 if it's nonexistent.
static pagesincategory( $parser, $name='', $arg1='', $arg2='')
Return the number of pages, files or subcats in the given category, or 0 if it's nonexistent.
static language( $parser, $code='', $inLanguage='')
Gives language names.
static bcp47(Parser $parser, string $code='')
Gives the BCP-47 code for a language given the mediawiki internal language code.
static numberofedits( $parser, $raw=null)
static namespacenumber( $parser, $title=null)
static revisiontimestamp( $parser, $title=null)
Get the timestamp from the last revision of a specified page.
static defaultsort( $parser, $text, $uarg='')
static basepagename( $parser, $title=null)
static formal(Parser $parser, string ... $forms)
static namespacee( $parser, $title=null)
static numberofpages( $parser, $raw=null)
static dir(Parser $parser, string $code='', string $arg='')
Gives direction of script of a language given a language code.
static numberingroup( $parser, $name='', $raw=null)
static interlanguagelink( $parser, $prefix='', $title='', $linkText=null)
static fullurle( $parser, $s='', $arg=null)
static pageid( $parser, $title=null)
Get the pageid of a specified page.
static intFunction( $parser, $part1='',... $params)
static pagesinnamespace( $parser, $namespace='0', $raw=null)
static tagObj( $parser, $frame, $args)
Parser function to extension tag adaptor.
static numberofusers( $parser, $raw=null)
static formatDate( $parser, $date, $defaultPref=null)
static talkspacee( $parser, $title=null)
static subjectpagenamee( $parser, $title=null)
static rootpagename( $parser, $title=null)
static cascadingsources( $parser, $title='')
Returns the sources of any cascading protection acting on a specified page.
static padright( $parser, $string='', $length='0', $padding='0')
static fullpagename( $parser, $title=null)
static formatRaw( $num, $raw, $language, ?MagicWordFactory $magicWordFactory=null)
Formats a number according to a language.
static plural( $parser, $text='',... $forms)
static subpagenamee( $parser, $title=null)
static fullpagenamee( $parser, $title=null)
static urlFunction( $func, $s='', $arg=null)
static talkpagenamee( $parser, $title=null)
static canonicalurl( $parser, $s='', $arg=null)
static pad( $parser, $string, $length, $padding='0', $direction=STR_PAD_RIGHT)
Unicode-safe str_pad with the restriction that $length is forced to be <= 500.
static revisionday2( $parser, $title=null)
Get the day with leading zeros from the last revision of a specified page.
static protectionexpiry( $parser, $type='', $title='')
Returns the requested protection expiry for the current page.
static padleft( $parser, $string='', $length='0', $padding='0')
static talkspace( $parser, $title=null)
static numberofactiveusers( $parser, $raw=null)
static subjectspacee( $parser, $title=null)
static pagenamee( $parser, $title=null)
static grammar( $parser, $case='', $word='')
static revisionuser( $parser, $title=null)
Get the user from the last revision of a specified page.
static localurl( $parser, $s='', $arg=null)
static urlencode( $parser, $s='', $arg=null)
urlencodes a string according to one of three patterns: (T24474)
static localurle( $parser, $s='', $arg=null)
static formatnum( $parser, $num='', $arg1='', $arg2='')
static rootpagenamee( $parser, $title=null)
static contentmodel(Parser $parser, ?string $format=null, ?string $title=null)
static protectionlevel( $parser, $type='', $title='')
Returns the requested protection level for the current page.
static subpagename( $parser, $title=null)
static subjectpagename( $parser, $title=null)
static interwikilink( $parser, $prefix='', $title='', $linkText=null)
static numberofadmins( $parser, $raw=null)
static revisionday( $parser, $title=null)
Get the day from the last revision of a specified page.
static canonicalurle( $parser, $s='', $arg=null)
static subjectspace( $parser, $title=null)
static revisionmonth( $parser, $title=null)
Get the month with leading zeros from the last revision of a specified page.
static basepagenamee( $parser, $title=null)
static gender( $parser, $username,... $forms)
static revisionyear( $parser, $title=null)
Get the year from the last revision of a specified page.
static pagename( $parser, $title=null)
Functions to get and normalize pagenames, corresponding to the magic words of the same names.
static fullurl( $parser, $s='', $arg=null)
static numberofarticles( $parser, $raw=null)
static revisionmonth1( $parser, $title=null)
Get the month from the last revision of a specified page.
static displaytitle( $parser, $text='', $uarg='')
Override the title of the page when viewed, provided we've been given a title which will normalise to...
static talkpagename( $parser, $title=null)
static revisionid( $parser, $title=null)
Get the id from the last revision of a specified page.
static numberoffiles( $parser, $raw=null)
static filepath( $parser, $name='', $argA='', $argB='')
Usage {{filepath|300}}, {{filepath|nowiki}}, {{filepath|nowiki|300}} or {{filepath|300|nowiki}} or {{...
Store information about magic words, and create/cache MagicWord objects.
get( $id)
Get a MagicWord object for a given internal ID.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:135
getTargetLanguageConverter()
Shorthand for getting a Language Converter for Target language.
Definition Parser.php:1587
markerSkipCallback( $s, callable $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:6300
tagNeedsNowikiStrippedInTagPF(string $lowerTagName)
Definition Parser.php:3940
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition Parser.php:1168
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:5062
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:6193
isCurrentRevisionOfTitleCached(LinkTarget $link)
Definition Parser.php:3550
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:6012
getContentLanguage()
Get the content language that this Parser is using.
Definition Parser.php:1178
parseWidthParam( $value, $parseHeight=true, bool $localized=false)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6348
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:6331
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:6107
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1114
msg(string $msg,... $params)
Helper function to correctly set the target language and title of a message based on the parser conte...
Definition Parser.php:4181
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1274
extensionSubstitution(array $params, PPFrame $frame, bool $processNowiki=false)
Return the text to be used for a given extension tag.
Definition Parser.php:3964
getRevisionRecordObject()
Get the revision record object for $this->mRevisionId.
Definition Parser.php:6022
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:6079
doQuotes( $text)
Helper function for handleAllQuotes()
Definition Parser.php:1921
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:820
fetchCurrentRevisionRecordOfTitle(LinkTarget $link)
Fetch the current revision of a given title as a RevisionRecord.
Definition Parser.php:3521
static stripAllTags(string $html)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed,...
static checkCss( $value)
Pick apart some CSS and check it for forbidden or unsafe structures.
static decodeCharReferencesAndNormalize(string $text)
Decode any character references, numeric or named entities, in the next and normalize the resulting s...
static removeSomeTags(string $text, array $options=[])
Cleans up HTML, removes dangerous tags and attributes, and removes HTML comments; the result will alw...
Exception representing a failure to look up a revision.
Page revision base class.
Static accessor class for site_stats and related things.
Definition SiteStats.php:22
Parent class for all special pages.
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:69
inNamespace(int $ns)
Returns true if the title is inside the specified namespace.
Definition Title.php:1295
equals(object $other)
Compares with another Title.
Definition Title.php:3063
getDBkey()
Get the main part with underscores.
Definition Title.php:1028
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1010
getDefaultOption(string $opt, ?UserIdentity $userIdentity=null)
Get a given default option value.
User class for the MediaWiki software.
Definition User.php:108
hasFragment()
Whether the link target has a fragment.