MediaWiki master
CoreParserFunctions.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Parser;
25
26use InvalidArgumentException;
31use MediaWiki\Languages\LanguageNameUtils;
43use Wikimedia\Bcp47Code\Bcp47CodeValue;
44use Wikimedia\RemexHtml\Tokenizer\Attributes;
45use Wikimedia\RemexHtml\Tokenizer\PlainAttributes;
46
53 private const MAX_TTS = 900;
54
58 public const REGISTER_OPTIONS = [
59 // See documentation for the corresponding config options
62 ];
63
71 public static function register( Parser $parser, ServiceOptions $options ) {
72 $options->assertRequiredOptions( self::REGISTER_OPTIONS );
73 $allowDisplayTitle = $options->get( MainConfigNames::AllowDisplayTitle );
74 $allowSlowParserFunctions = $options->get( MainConfigNames::AllowSlowParserFunctions );
75
76 # Syntax for arguments (see Parser::setFunctionHook):
77 # "name for lookup in localized magic words array",
78 # function callback,
79 # optional Parser::SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}}
80 # instead of {{#int:...}})
81 $noHashFunctions = [
82 'ns', 'nse', 'urlencode', 'lcfirst', 'ucfirst', 'lc', 'uc',
83 'localurl', 'localurle', 'fullurl', 'fullurle', 'canonicalurl',
84 'canonicalurle', 'formatnum', 'grammar', 'gender', 'plural', 'formal',
85 'bidi', 'numberingroup', 'language',
86 'padleft', 'padright', 'anchorencode', 'defaultsort', 'filepath',
87 'pagesincategory', 'pagesize', 'protectionlevel', 'protectionexpiry',
88 # The following are the "parser function" forms of magic
89 # variables defined in CoreMagicVariables. The no-args form will
90 # go through the magic variable code path (and be cached); the
91 # presence of arguments will cause the parser function form to
92 # be invoked. (Note that the actual implementation will pass
93 # a Parser object as first argument, in addition to the
94 # parser function parameters.)
95
96 # For this group, the first parameter to the parser function is
97 # "page title", and the no-args form (and the magic variable)
98 # defaults to "current page title".
99 'pagename', 'pagenamee',
100 'fullpagename', 'fullpagenamee',
101 'subpagename', 'subpagenamee',
102 'rootpagename', 'rootpagenamee',
103 'basepagename', 'basepagenamee',
104 'talkpagename', 'talkpagenamee',
105 'subjectpagename', 'subjectpagenamee',
106 'pageid', 'revisionid', 'revisionday',
107 'revisionday2', 'revisionmonth', 'revisionmonth1', 'revisionyear',
108 'revisiontimestamp',
109 'revisionuser',
110 'cascadingsources',
111 'namespace', 'namespacee', 'namespacenumber', 'talkspace', 'talkspacee',
112 'subjectspace', 'subjectspacee',
113
114 # More parser functions corresponding to CoreMagicVariables.
115 # For this group, the first parameter to the parser function is
116 # "raw" (uses the 'raw' format if present) and the no-args form
117 # (and the magic variable) defaults to 'not raw'.
118 'numberofarticles', 'numberoffiles',
119 'numberofusers',
120 'numberofactiveusers',
121 'numberofpages',
122 'numberofadmins',
123 'numberofedits',
124
125 # These magic words already contain the hash, and the no-args form
126 # is the same as passing an empty first argument
127 'bcp47',
128 'dir',
129 'interwikilink',
130 'interlanguagelink',
131 ];
132 foreach ( $noHashFunctions as $func ) {
133 $parser->setFunctionHook( $func, [ self::class, $func ], Parser::SFH_NO_HASH );
134 }
135
136 $parser->setFunctionHook( 'int', [ self::class, 'intFunction' ], Parser::SFH_NO_HASH );
137 $parser->setFunctionHook( 'special', [ self::class, 'special' ] );
138 $parser->setFunctionHook( 'speciale', [ self::class, 'speciale' ] );
139 $parser->setFunctionHook( 'tag', [ self::class, 'tagObj' ], Parser::SFH_OBJECT_ARGS );
140 $parser->setFunctionHook( 'formatdate', [ self::class, 'formatDate' ] );
141
142 if ( $allowDisplayTitle ) {
143 $parser->setFunctionHook(
144 'displaytitle',
145 [ self::class, 'displaytitle' ],
147 );
148 }
149 if ( $allowSlowParserFunctions ) {
150 $parser->setFunctionHook(
151 'pagesinnamespace',
152 [ self::class, 'pagesinnamespace' ],
154 );
155 }
156 }
157
164 public static function intFunction( $parser, $part1 = '', ...$params ) {
165 if ( strval( $part1 ) !== '' ) {
166 $message = wfMessage( $part1, $params )
167 ->inLanguage( $parser->getOptions()->getUserLangObj() );
168 return [ $message->plain(), 'noparse' => false ];
169 } else {
170 return [ 'found' => false ];
171 }
172 }
173
181 public static function formatDate( $parser, $date, $defaultPref = null ) {
182 $lang = $parser->getTargetLanguage();
183 $df = MediaWikiServices::getInstance()->getDateFormatterFactory()->get( $lang );
184
185 $date = trim( $date );
186
187 $pref = $parser->getOptions()->getDateFormat();
188
189 // Specify a different default date format other than the normal default
190 // if the user has 'default' for their setting
191 if ( $pref == 'default' && $defaultPref ) {
192 $pref = $defaultPref;
193 }
194
195 $date = $df->reformat( $pref, $date, [ 'match-whole' ] );
196 return $date;
197 }
198
199 public static function ns( $parser, $part1 = '' ) {
200 if ( intval( $part1 ) || $part1 == "0" ) {
201 $index = intval( $part1 );
202 } else {
203 $index = $parser->getContentLanguage()->getNsIndex( str_replace( ' ', '_', $part1 ) );
204 }
205 if ( $index !== false ) {
206 return $parser->getContentLanguage()->getFormattedNsText( $index );
207 } else {
208 return [ 'found' => false ];
209 }
210 }
211
212 public static function nse( $parser, $part1 = '' ) {
213 $ret = self::ns( $parser, $part1 );
214 if ( is_string( $ret ) ) {
215 $ret = wfUrlencode( str_replace( ' ', '_', $ret ) );
216 }
217 return $ret;
218 }
219
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' ] );
237 }
238 switch ( $magicWords->matchStartToEnd( $arg ?? '' ) ) {
239 // Encode as though it's a wiki page, '_' for ' '.
240 case 'url_wiki':
241 $func = 'wfUrlencode';
242 $s = str_replace( ' ', '_', $s );
243 break;
244
245 // Encode for an HTTP Path, '%20' for ' '.
246 case 'url_path':
247 $func = 'rawurlencode';
248 break;
249
250 // Encode for HTTP query, '+' for ' '.
251 case 'url_query':
252 default:
253 $func = 'urlencode';
254 }
255 // See T105242, where the choice to kill markers and various
256 // other options were discussed.
257 return $func( $parser->killMarkers( $s ) );
258 }
259
260 public static function lcfirst( $parser, $s = '' ) {
261 return $parser->getContentLanguage()->lcfirst( $s );
262 }
263
264 public static function ucfirst( $parser, $s = '' ) {
265 return $parser->getContentLanguage()->ucfirst( $s );
266 }
267
273 public static function lc( $parser, $s = '' ) {
274 return $parser->markerSkipCallback( $s, [ $parser->getContentLanguage(), 'lc' ] );
275 }
276
282 public static function uc( $parser, $s = '' ) {
283 return $parser->markerSkipCallback( $s, [ $parser->getContentLanguage(), 'uc' ] );
284 }
285
286 public static function localurl( $parser, $s = '', $arg = null ) {
287 return self::urlFunction( 'getLocalURL', $s, $arg );
288 }
289
290 public static function localurle( $parser, $s = '', $arg = null ) {
291 $temp = self::urlFunction( 'getLocalURL', $s, $arg );
292 if ( !is_string( $temp ) ) {
293 return $temp;
294 } else {
295 return htmlspecialchars( $temp, ENT_COMPAT );
296 }
297 }
298
299 public static function fullurl( $parser, $s = '', $arg = null ) {
300 return self::urlFunction( 'getFullURL', $s, $arg );
301 }
302
303 public static function fullurle( $parser, $s = '', $arg = null ) {
304 $temp = self::urlFunction( 'getFullURL', $s, $arg );
305 if ( !is_string( $temp ) ) {
306 return $temp;
307 } else {
308 return htmlspecialchars( $temp, ENT_COMPAT );
309 }
310 }
311
312 public static function canonicalurl( $parser, $s = '', $arg = null ) {
313 return self::urlFunction( 'getCanonicalURL', $s, $arg );
314 }
315
316 public static function canonicalurle( $parser, $s = '', $arg = null ) {
317 $temp = self::urlFunction( 'getCanonicalURL', $s, $arg );
318 if ( !is_string( $temp ) ) {
319 return $temp;
320 } else {
321 return htmlspecialchars( $temp, ENT_COMPAT );
322 }
323 }
324
325 public static function urlFunction( $func, $s = '', $arg = null ) {
326 # Due to order of execution of a lot of bits, the values might be encoded
327 # before arriving here; if that's true, then the title can't be created
328 # and the variable will fail. If we can't get a decent title from the first
329 # attempt, url-decode and try for a second.
330 $title = Title::newFromText( $s ) ?? Title::newFromURL( urldecode( $s ) );
331 if ( $title !== null ) {
332 # Convert NS_MEDIA -> NS_FILE
333 if ( $title->inNamespace( NS_MEDIA ) ) {
334 $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
335 }
336 if ( $arg !== null ) {
337 $text = $title->$func( $arg );
338 } else {
339 $text = $title->$func();
340 }
341 return $text;
342 } else {
343 return [ 'found' => false ];
344 }
345 }
346
354 public static function formatnum( $parser, $num = '', $arg1 = '', $arg2 = '' ) {
355 static $magicWords = null;
356 if ( $magicWords === null ) {
357 $magicWords = $parser->getMagicWordFactory()->newArray( [
358 'rawsuffix',
359 'nocommafysuffix',
360 'lossless',
361 ] );
362 }
363
364 $modifiers = [ $magicWords->matchStartToEnd( $arg1 ), $magicWords->matchStartToEnd( $arg2 ) ];
365 $targetLanguage = $parser->getTargetLanguage();
366 if ( in_array( 'rawsuffix', $modifiers, true ) ) {
367 $func = [ $targetLanguage, 'parseFormattedNumber' ];
368 } else {
369 if ( in_array( 'nocommafysuffix', $modifiers, true ) ) {
370 $func = [ $targetLanguage, 'formatNumNoSeparators' ];
371 } else {
372 $func = [ $targetLanguage, 'formatNum' ];
373 $func = self::getLegacyFormatNum( $parser, $func );
374 }
375 if ( in_array( 'lossless', $modifiers, true ) ) {
376 $potentiallyLossyFunc = $func;
377 $func = static function ( $num ) use ( $targetLanguage, $potentiallyLossyFunc ) {
378 $formatted = $potentiallyLossyFunc( $num );
379 $parsed = $targetLanguage->parseFormattedNumber( $formatted );
380 if ( $num === $parsed ) {
381 return $formatted;
382 } else {
383 return (string)$num;
384 }
385 };
386 }
387 }
388 return $parser->markerSkipCallback( $num, $func );
389 }
390
397 private static function getLegacyFormatNum( $parser, $callback ) {
398 // For historic reasons, the formatNum parser function will
399 // take arguments which are not actually formatted numbers,
400 // which then trigger deprecation warnings in Language::formatNum*.
401 // Instead emit a tracking category instead to allow linting.
402 return static function ( $number ) use ( $parser, $callback ) {
403 $validNumberRe = '(-(?=[\d\.]))?(\d+|(?=\.\d))(\.\d*)?([Ee][-+]?\d+)?';
404 if (
405 !is_numeric( $number ) &&
406 $number !== (string)NAN &&
407 $number !== (string)INF &&
408 $number !== (string)-INF
409 ) {
410 $parser->addTrackingCategory( 'nonnumeric-formatnum' );
411 // Don't split on NAN/INF in the legacy case since they are
412 // likely to be found embedded inside non-numeric text.
413 return preg_replace_callback( "/{$validNumberRe}/", static function ( $m ) use ( $callback ) {
414 return $callback( $m[0] );
415 }, $number );
416 }
417 return $callback( $number );
418 };
419 }
420
427 public static function grammar( $parser, $case = '', $word = '' ) {
428 $word = $parser->killMarkers( $word );
429 return $parser->getTargetLanguage()->convertGrammar( $word, $case );
430 }
431
438 public static function gender( $parser, $username, ...$forms ) {
439 // Some shortcuts to avoid loading user data unnecessarily
440 if ( count( $forms ) === 0 ) {
441 return '';
442 } elseif ( count( $forms ) === 1 ) {
443 return $forms[0];
444 }
445
446 $username = trim( $username );
447
448 $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
449 $gender = $userOptionsLookup->getDefaultOption( 'gender' );
450
451 // allow prefix and normalize (e.g. "&#42;foo" -> "*foo" ).
452 $title = Title::newFromText( $username, NS_USER );
453
454 if ( $title && $title->inNamespace( NS_USER ) ) {
455 $username = $title->getText();
456 }
457
458 // check parameter, or use the ParserOptions if in interface message
459 $user = User::newFromName( $username );
460 $genderCache = MediaWikiServices::getInstance()->getGenderCache();
461 if ( $user ) {
462 $gender = $genderCache->getGenderOf( $user, __METHOD__ );
463 } elseif ( $username === '' && $parser->getOptions()->isMessage() ) {
464 $gender = $genderCache->getGenderOf( $parser->getOptions()->getUserIdentity(), __METHOD__ );
465 }
466 $ret = $parser->getTargetLanguage()->gender( $gender, $forms );
467 return $ret;
468 }
469
476 public static function plural( $parser, $text = '', ...$forms ) {
477 $text = $parser->getTargetLanguage()->parseFormattedNumber( $text );
478 settype( $text, ctype_digit( $text ) ? 'int' : 'float' );
479 // @phan-suppress-next-line PhanTypeMismatchArgument Phan does not handle settype
480 return $parser->getTargetLanguage()->convertPlural( $text, $forms );
481 }
482
483 public static function formal( Parser $parser, string ...$forms ): string {
484 $index = $parser->getTargetLanguage()->getFormalityIndex();
485 return $forms[$index] ?? $forms[0];
486 }
487
493 public static function bidi( $parser, $text = '' ) {
494 return $parser->getTargetLanguage()->embedBidi( $text );
495 }
496
506 public static function displaytitle( $parser, $text = '', $uarg = '' ) {
507 $restrictDisplayTitle = MediaWikiServices::getInstance()->getMainConfig()
509
510 static $magicWords = null;
511 if ( $magicWords === null ) {
512 $magicWords = $parser->getMagicWordFactory()->newArray(
513 [ 'displaytitle_noerror', 'displaytitle_noreplace' ] );
514 }
515 $arg = $magicWords->matchStartToEnd( $uarg );
516
517 // parse a limited subset of wiki markup (just the single quote items)
518 $text = $parser->doQuotes( $text );
519
520 // remove stripped text (e.g. the UNIQ-QINU stuff) that was generated by tag extensions/whatever
521 $text = $parser->killMarkers( $text );
522
523 // See T28547 for rationale for this processing.
524 // list of disallowed tags for DISPLAYTITLE
525 // these will be escaped even though they are allowed in normal wiki text
526 $bad = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'blockquote', 'ol', 'ul', 'li', 'hr',
527 'table', 'tr', 'th', 'td', 'dl', 'dd', 'caption', 'p', 'ruby', 'rb', 'rt', 'rtc', 'rp', 'br' ];
528
529 // disallow some styles that could be used to bypass $wgRestrictDisplayTitle
530 if ( $restrictDisplayTitle ) {
531 // This code is tested with the cases marked T28547 in
532 // parserTests.txt
533 $htmlTagsCallback = static function ( Attributes $attr ): Attributes {
534 $decoded = $attr->getValues();
535
536 if ( isset( $decoded['style'] ) ) {
537 // this is called later anyway, but we need it right now for the regexes below to be safe
538 // calling it twice doesn't hurt
539 $decoded['style'] = Sanitizer::checkCss( $decoded['style'] );
540
541 if ( preg_match( '/(display|user-select|visibility)\s*:/i', $decoded['style'] ) ) {
542 $decoded['style'] = '/* attempt to bypass $wgRestrictDisplayTitle */';
543 }
544 }
545
546 return new PlainAttributes( $decoded );
547 };
548 } else {
549 $htmlTagsCallback = null;
550 }
551
552 // only requested titles that normalize to the actual title are allowed through
553 // if $wgRestrictDisplayTitle is true (it is by default)
554 // mimic the escaping process that occurs in OutputPage::setPageTitle
555 $text = Sanitizer::removeSomeTags( $text, [
556 'attrCallback' => $htmlTagsCallback,
557 'removeTags' => $bad,
558 ] );
559 $title = Title::newFromText( Sanitizer::stripAllTags( $text ) );
560 // Decode entities in $text the same way that Title::newFromText does
561 $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
562
563 if ( !$restrictDisplayTitle ||
564 ( $title instanceof Title
565 && !$title->hasFragment()
566 && $title->equals( $parser->getTitle() ) )
567 ) {
568 $old = $parser->getOutput()->getPageProperty( 'displaytitle' );
569 if ( $old === null || $arg !== 'displaytitle_noreplace' ) {
570 $parser->getOutput()->setDisplayTitle( $text );
571 }
572 if ( $old !== null && $old !== $text && !$arg ) {
573
574 $converter = $parser->getTargetLanguageConverter();
575 return '<span class="error">' .
576 $parser->msg( 'duplicate-displaytitle',
577 // Message should be parsed, but these params should only be escaped.
578 $converter->markNoConversion( wfEscapeWikiText( $old ) ),
579 $converter->markNoConversion( wfEscapeWikiText( $filteredText ) )
580 )->text() .
581 '</span>';
582 } else {
583 return '';
584 }
585 } else {
586 $parser->getOutput()->addWarningMsg(
587 'restricted-displaytitle',
588 // Message should be parsed, but this param should only be escaped.
589 Message::plaintextParam( $filteredText )
590 );
591 $parser->addTrackingCategory( 'restricted-displaytitle-ignored' );
592 }
593 }
594
604 private static function matchAgainstMagicword(
605 MagicWordFactory $magicWordFactory, $magicword, $value
606 ) {
607 $value = trim( strval( $value ) );
608 if ( $value === '' ) {
609 return false;
610 }
611 $mwObject = $magicWordFactory->get( $magicword );
612 return $mwObject->matchStartToEnd( $value );
613 }
614
624 public static function formatRaw(
625 $num, $raw, $language, ?MagicWordFactory $magicWordFactory = null
626 ) {
627 if ( $raw !== null && $raw !== '' ) {
628 if ( !$magicWordFactory ) {
629 $magicWordFactory = MediaWikiServices::getInstance()->getMagicWordFactory();
630 }
631 if ( self::matchAgainstMagicword( $magicWordFactory, 'rawsuffix', $raw ) ) {
632 return (string)$num;
633 }
634 }
635 return $language->formatNum( $num );
636 }
637
638 public static function numberofpages( $parser, $raw = null ) {
639 return self::formatRaw( SiteStats::pages(), $raw, $parser->getTargetLanguage() );
640 }
641
642 public static function numberofusers( $parser, $raw = null ) {
643 return self::formatRaw( SiteStats::users(), $raw, $parser->getTargetLanguage() );
644 }
645
646 public static function numberofactiveusers( $parser, $raw = null ) {
647 return self::formatRaw( SiteStats::activeUsers(), $raw, $parser->getTargetLanguage() );
648 }
649
650 public static function numberofarticles( $parser, $raw = null ) {
651 return self::formatRaw( SiteStats::articles(), $raw, $parser->getTargetLanguage() );
652 }
653
654 public static function numberoffiles( $parser, $raw = null ) {
655 return self::formatRaw( SiteStats::images(), $raw, $parser->getTargetLanguage() );
656 }
657
658 public static function numberofadmins( $parser, $raw = null ) {
659 return self::formatRaw(
660 SiteStats::numberingroup( 'sysop' ),
661 $raw,
662 $parser->getTargetLanguage()
663 );
664 }
665
666 public static function numberofedits( $parser, $raw = null ) {
667 return self::formatRaw( SiteStats::edits(), $raw, $parser->getTargetLanguage() );
668 }
669
670 public static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) {
671 return self::formatRaw(
672 SiteStats::pagesInNs( intval( $namespace ) ),
673 $raw,
674 $parser->getTargetLanguage()
675 );
676 }
677
678 public static function numberingroup( $parser, $name = '', $raw = null ) {
679 return self::formatRaw(
680 SiteStats::numberingroup( strtolower( $name ) ),
681 $raw,
682 $parser->getTargetLanguage()
683 );
684 }
685
694 private static function makeTitle( Parser $parser, ?string $t ) {
695 if ( $t === null ) {
696 // For consistency with magic variable forms
697 $title = $parser->getTitle();
698 } else {
699 $title = Title::newFromText( $t );
700 }
701 return $title;
702 }
703
712 public static function namespace( $parser, $title = null ) {
713 $t = self::makeTitle( $parser, $title );
714 if ( $t === null ) {
715 return '';
716 }
717 return str_replace( '_', ' ', $t->getNsText() );
718 }
719
720 public static function namespacee( $parser, $title = null ) {
721 $t = self::makeTitle( $parser, $title );
722 if ( $t === null ) {
723 return '';
724 }
725 return wfUrlencode( $t->getNsText() );
726 }
727
728 public static function namespacenumber( $parser, $title = null ) {
729 $t = self::makeTitle( $parser, $title );
730 if ( $t === null ) {
731 return '';
732 }
733 return (string)$t->getNamespace();
734 }
735
736 public static function talkspace( $parser, $title = null ) {
737 $t = self::makeTitle( $parser, $title );
738 if ( $t === null || !$t->canHaveTalkPage() ) {
739 return '';
740 }
741 return str_replace( '_', ' ', $t->getTalkNsText() );
742 }
743
744 public static function talkspacee( $parser, $title = null ) {
745 $t = self::makeTitle( $parser, $title );
746 if ( $t === null || !$t->canHaveTalkPage() ) {
747 return '';
748 }
749 return wfUrlencode( $t->getTalkNsText() );
750 }
751
752 public static function subjectspace( $parser, $title = null ) {
753 $t = self::makeTitle( $parser, $title );
754 if ( $t === null ) {
755 return '';
756 }
757 return str_replace( '_', ' ', $t->getSubjectNsText() );
758 }
759
760 public static function subjectspacee( $parser, $title = null ) {
761 $t = self::makeTitle( $parser, $title );
762 if ( $t === null ) {
763 return '';
764 }
765 return wfUrlencode( $t->getSubjectNsText() );
766 }
767
775 public static function pagename( $parser, $title = null ) {
776 $t = self::makeTitle( $parser, $title );
777 if ( $t === null ) {
778 return '';
779 }
780 return wfEscapeWikiText( $t->getText() );
781 }
782
783 public static function pagenamee( $parser, $title = null ) {
784 $t = self::makeTitle( $parser, $title );
785 if ( $t === null ) {
786 return '';
787 }
788 return wfEscapeWikiText( $t->getPartialURL() );
789 }
790
791 public static function fullpagename( $parser, $title = null ) {
792 $t = self::makeTitle( $parser, $title );
793 if ( $t === null ) {
794 return '';
795 }
796 return wfEscapeWikiText( $t->getPrefixedText() );
797 }
798
799 public static function fullpagenamee( $parser, $title = null ) {
800 $t = self::makeTitle( $parser, $title );
801 if ( $t === null ) {
802 return '';
803 }
804 return wfEscapeWikiText( $t->getPrefixedURL() );
805 }
806
807 public static function subpagename( $parser, $title = null ) {
808 $t = self::makeTitle( $parser, $title );
809 if ( $t === null ) {
810 return '';
811 }
812 return wfEscapeWikiText( $t->getSubpageText() );
813 }
814
815 public static function subpagenamee( $parser, $title = null ) {
816 $t = self::makeTitle( $parser, $title );
817 if ( $t === null ) {
818 return '';
819 }
820 return wfEscapeWikiText( $t->getSubpageUrlForm() );
821 }
822
823 public static function rootpagename( $parser, $title = null ) {
824 $t = self::makeTitle( $parser, $title );
825 if ( $t === null ) {
826 return '';
827 }
828 return wfEscapeWikiText( $t->getRootText() );
829 }
830
831 public static function rootpagenamee( $parser, $title = null ) {
832 $t = self::makeTitle( $parser, $title );
833 if ( $t === null ) {
834 return '';
835 }
836 return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getRootText() ) ) );
837 }
838
839 public static function basepagename( $parser, $title = null ) {
840 $t = self::makeTitle( $parser, $title );
841 if ( $t === null ) {
842 return '';
843 }
844 return wfEscapeWikiText( $t->getBaseText() );
845 }
846
847 public static function basepagenamee( $parser, $title = null ) {
848 $t = self::makeTitle( $parser, $title );
849 if ( $t === null ) {
850 return '';
851 }
852 return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getBaseText() ) ) );
853 }
854
855 public static function talkpagename( $parser, $title = null ) {
856 $t = self::makeTitle( $parser, $title );
857 if ( $t === null || !$t->canHaveTalkPage() ) {
858 return '';
859 }
860 return wfEscapeWikiText( $t->getTalkPage()->getPrefixedText() );
861 }
862
863 public static function talkpagenamee( $parser, $title = null ) {
864 $t = self::makeTitle( $parser, $title );
865 if ( $t === null || !$t->canHaveTalkPage() ) {
866 return '';
867 }
868 return wfEscapeWikiText( $t->getTalkPage()->getPrefixedURL() );
869 }
870
871 public static function subjectpagename( $parser, $title = null ) {
872 $t = self::makeTitle( $parser, $title );
873 if ( $t === null ) {
874 return '';
875 }
876 return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedText() );
877 }
878
879 public static function subjectpagenamee( $parser, $title = null ) {
880 $t = self::makeTitle( $parser, $title );
881 if ( $t === null ) {
882 return '';
883 }
884 return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedURL() );
885 }
886
897 public static function pagesincategory( $parser, $name = '', $arg1 = '', $arg2 = '' ) {
898 static $magicWords = null;
899 if ( $magicWords === null ) {
900 $magicWords = $parser->getMagicWordFactory()->newArray( [
901 'pagesincategory_all',
902 'pagesincategory_pages',
903 'pagesincategory_subcats',
904 'pagesincategory_files'
905 ] );
906 }
907 static $cache = [];
908
909 // split the given option to its variable
910 if ( self::matchAgainstMagicword( $parser->getMagicWordFactory(), 'rawsuffix', $arg1 ) ) {
911 // {{pagesincategory:|raw[|type]}}
912 $raw = $arg1;
913 $type = $magicWords->matchStartToEnd( $arg2 );
914 } else {
915 // {{pagesincategory:[|type[|raw]]}}
916 $type = $magicWords->matchStartToEnd( $arg1 );
917 $raw = $arg2;
918 }
919 if ( !$type ) { // backward compatibility
920 $type = 'pagesincategory_all';
921 }
922
923 $title = Title::makeTitleSafe( NS_CATEGORY, $name );
924 if ( !$title ) { # invalid title
925 return self::formatRaw( 0, $raw, $parser->getTargetLanguage() );
926 }
927 $languageConverter = MediaWikiServices::getInstance()
928 ->getLanguageConverterFactory()
929 ->getLanguageConverter( $parser->getContentLanguage() );
930 $languageConverter->findVariantLink( $name, $title, true );
931
932 // Normalize name for cache
933 $name = $title->getDBkey();
934
935 if ( !isset( $cache[$name] ) ) {
936 $category = Category::newFromTitle( $title );
937
938 $allCount = $subcatCount = $fileCount = $pageCount = 0;
939 if ( $parser->incrementExpensiveFunctionCount() ) {
940 $allCount = $category->getMemberCount();
941 $subcatCount = $category->getSubcatCount();
942 $fileCount = $category->getFileCount();
943 $pageCount = $category->getPageCount( Category::COUNT_CONTENT_PAGES );
944 }
945 $cache[$name]['pagesincategory_all'] = $allCount;
946 $cache[$name]['pagesincategory_pages'] = $pageCount;
947 $cache[$name]['pagesincategory_subcats'] = $subcatCount;
948 $cache[$name]['pagesincategory_files'] = $fileCount;
949 }
950
951 $count = $cache[$name][$type];
952 return self::formatRaw( $count, $raw, $parser->getTargetLanguage() );
953 }
954
964 public static function pagesize( $parser, $page = '', $raw = null ) {
965 $title = Title::newFromText( $page );
966
967 if ( !is_object( $title ) || $title->isExternal() ) {
968 return self::formatRaw( 0, $raw, $parser->getTargetLanguage() );
969 }
970
971 // fetch revision from cache/database and return the value
972 $rev = self::getCachedRevisionObject( $parser, $title, ParserOutputFlags::VARY_REVISION_SHA1 );
973 $length = $rev ? $rev->getSize() : 0;
974 if ( $length === null ) {
975 // We've had bugs where rev_len was not being recorded for empty pages, see T135414
976 $length = 0;
977 }
978 return self::formatRaw( $length, $raw, $parser->getTargetLanguage() );
979 }
980
993 public static function protectionlevel( $parser, $type = '', $title = '' ) {
994 $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
995 $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
996 if ( $restrictionStore->areRestrictionsLoaded( $titleObject ) || $parser->incrementExpensiveFunctionCount() ) {
997 $restrictions = $restrictionStore->getRestrictions( $titleObject, strtolower( $type ) );
998 # RestrictionStore::getRestrictions returns an array, its possible it may have
999 # multiple values in the future
1000 return implode( ',', $restrictions );
1001 }
1002 return '';
1003 }
1004
1017 public static function protectionexpiry( $parser, $type = '', $title = '' ) {
1018 $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
1019 $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
1020 if ( $restrictionStore->areRestrictionsLoaded( $titleObject ) || $parser->incrementExpensiveFunctionCount() ) {
1021 // getRestrictionExpiry() returns null on invalid type; trying to
1022 // match protectionlevel() function that returns empty string instead
1023 return $restrictionStore->getRestrictionExpiry( $titleObject, strtolower( $type ) ) ?? '';
1024 }
1025 return '';
1026 }
1027
1036 public static function language( $parser, $code = '', $inLanguage = '' ) {
1037 if ( $code === '' ) {
1038 $code = $parser->getTargetLanguage()->getCode();
1039 }
1040 if ( $inLanguage === '' ) {
1041 $inLanguage = LanguageNameUtils::AUTONYMS;
1042 }
1044 ->getLanguageNameUtils()
1045 ->getLanguageName( $code, $inLanguage );
1046 return $lang !== '' ? $lang : LanguageCode::bcp47( $code );
1047 }
1048
1060 public static function dir( Parser $parser, string $code = '', string $arg = '' ): string {
1061 static $magicWords = null;
1062 $languageFactory = MediaWikiServices::getInstance()->getLanguageFactory();
1063
1064 if ( $code === '' ) {
1065 $lang = $parser->getTargetLanguage();
1066 } else {
1067 if ( $arg !== '' ) {
1068 if ( $magicWords === null ) {
1069 $magicWords = $parser->getMagicWordFactory()->newArray( [ 'language_option_bcp47' ] );
1070 }
1071 if ( $magicWords->matchStartToEnd( $arg ) === 'language_option_bcp47' ) {
1072 // Prefer the BCP-47 interpretation of this code.
1073 $code = new Bcp47CodeValue( $code );
1074 }
1075 }
1076 try {
1077 $lang = $languageFactory->getLanguage( $code );
1078 } catch ( InvalidArgumentException ) {
1079 $parser->addTrackingCategory( 'bad-language-code-category' );
1080 return 'ltr';
1081 }
1082 }
1083 return $lang->getDir();
1084 }
1085
1094 public static function bcp47( Parser $parser, string $code = '' ): string {
1095 if ( $code === '' ) {
1096 return $parser->getTargetLanguage()->toBcp47Code();
1097 } else {
1098 return LanguageCode::bcp47( $code );
1099 }
1100 }
1101
1111 public static function pad(
1112 $parser, $string, $length, $padding = '0', $direction = STR_PAD_RIGHT
1113 ) {
1114 $padding = $parser->killMarkers( $padding );
1115 $lengthOfPadding = mb_strlen( $padding );
1116 if ( $lengthOfPadding == 0 ) {
1117 return $string;
1118 }
1119
1120 # The remaining length to add counts down to 0 as padding is added
1121 $length = min( (int)$length, 500 ) - mb_strlen( $string );
1122 if ( $length <= 0 ) {
1123 // Nothing to add
1124 return $string;
1125 }
1126
1127 # $finalPadding is just $padding repeated enough times so that
1128 # mb_strlen( $string ) + mb_strlen( $finalPadding ) == $length
1129 $finalPadding = '';
1130 while ( $length > 0 ) {
1131 # If $length < $lengthofPadding, truncate $padding so we get the
1132 # exact length desired.
1133 $finalPadding .= mb_substr( $padding, 0, $length );
1134 $length -= $lengthOfPadding;
1135 }
1136
1137 if ( $direction == STR_PAD_LEFT ) {
1138 return $finalPadding . $string;
1139 } else {
1140 return $string . $finalPadding;
1141 }
1142 }
1143
1144 public static function padleft( $parser, $string = '', $length = 0, $padding = '0' ) {
1145 return self::pad( $parser, $string, $length, $padding, STR_PAD_LEFT );
1146 }
1147
1148 public static function padright( $parser, $string = '', $length = 0, $padding = '0' ) {
1149 return self::pad( $parser, $string, $length, $padding );
1150 }
1151
1157 public static function anchorencode( $parser, $text ) {
1158 $text = $parser->killMarkers( $text );
1159 $section = substr( $parser->guessSectionNameFromWikiText( $text ), 1 );
1160 return Sanitizer::safeEncodeAttribute( $section );
1161 }
1162
1163 public static function special( $parser, $text ) {
1164 [ $page, $subpage ] = MediaWikiServices::getInstance()->getSpecialPageFactory()->
1165 resolveAlias( $text );
1166 if ( $page ) {
1167 $title = SpecialPage::getTitleFor( $page, $subpage );
1168 return $title->getPrefixedText();
1169 } else {
1170 // unknown special page, just use the given text as its title, if at all possible
1171 $title = Title::makeTitleSafe( NS_SPECIAL, $text );
1172 return $title ? $title->getPrefixedText() : self::special( $parser, 'Badtitle' );
1173 }
1174 }
1175
1176 public static function speciale( $parser, $text ) {
1177 return wfUrlencode( str_replace( ' ', '_', self::special( $parser, $text ) ) );
1178 }
1179
1188 public static function defaultsort( $parser, $text, $uarg = '' ) {
1189 static $magicWords = null;
1190 if ( $magicWords === null ) {
1191 $magicWords = $parser->getMagicWordFactory()->newArray(
1192 [ 'defaultsort_noerror', 'defaultsort_noreplace' ] );
1193 }
1194 $arg = $magicWords->matchStartToEnd( $uarg );
1195
1196 $text = trim( $text );
1197 if ( $text === '' ) {
1198 return '';
1199 }
1200 $old = $parser->getOutput()->getPageProperty( 'defaultsort' );
1201 if ( $old === null || $arg !== 'defaultsort_noreplace' ) {
1202 $parser->getOutput()->setPageProperty( 'defaultsort', $text );
1203 }
1204
1205 if ( $old === null || $old == $text || $arg ) {
1206 return '';
1207 } else {
1208 $converter = $parser->getTargetLanguageConverter();
1209 return '<span class="error">' .
1210 $parser->msg( 'duplicate-defaultsort',
1211 // Message should be parsed, but these params should only be escaped.
1212 $converter->markNoConversion( wfEscapeWikiText( $old ) ),
1213 $converter->markNoConversion( wfEscapeWikiText( $text ) )
1214 )->text() .
1215 '</span>';
1216 }
1217 }
1218
1230 public static function filepath( $parser, $name = '', $argA = '', $argB = '' ) {
1231 $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $name );
1232
1233 if ( $argA == 'nowiki' ) {
1234 // {{filepath: | option [| size] }}
1235 $isNowiki = true;
1236 $parsedWidthParam = $parser->parseWidthParam( $argB );
1237 } else {
1238 // {{filepath: [| size [|option]] }}
1239 $parsedWidthParam = $parser->parseWidthParam( $argA );
1240 $isNowiki = ( $argB == 'nowiki' );
1241 }
1242
1243 if ( $file ) {
1244 $url = $file->getFullUrl();
1245
1246 // If a size is requested...
1247 if ( count( $parsedWidthParam ) ) {
1248 $mto = $file->transform( $parsedWidthParam );
1249 // ... and we can
1250 if ( $mto && !$mto->isError() ) {
1251 // ... change the URL to point to a thumbnail.
1252 $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
1253 $url = $urlUtils->expand( $mto->getUrl(), PROTO_RELATIVE ) ?? false;
1254 }
1255 }
1256 if ( $isNowiki ) {
1257 return [ $url, 'nowiki' => true ];
1258 }
1259 return $url;
1260 } else {
1261 return '';
1262 }
1263 }
1264
1272 public static function tagObj( $parser, $frame, $args ) {
1273 if ( !count( $args ) ) {
1274 return '';
1275 }
1276 $tagName = strtolower( trim( $parser->killMarkers(
1277 $frame->expand( array_shift( $args ) )
1278 ) ) );
1279 $processNowiki = $parser->tagNeedsNowikiStrippedInTagPF( $tagName ) ? PPFrame::PROCESS_NOWIKI : 0;
1280
1281 if ( count( $args ) ) {
1282 // With Parsoid Fragment support, $processNoWiki flag
1283 // isn't actually required as a ::expand() flag, but it
1284 // doesn't do any harm.
1285 $inner = $frame->expand( array_shift( $args ), $processNowiki );
1286 if ( $processNowiki ) {
1287 // This is the T299103 workaround for <syntaxhighlight>,
1288 // and reproduces the code in SyntaxHighlight::parserHook.
1289 // The Parsoid extension API (SyntaxHighlight::sourceToDom)
1290 // doesn't (yet) know about strip state, and so can't do
1291 // this itself.
1292 $inner = $parser->getStripState()->unstripNoWiki( $inner );
1293 }
1294 } else {
1295 $inner = null;
1296 }
1297
1298 $attributes = [];
1299 foreach ( $args as $arg ) {
1300 $bits = $arg->splitArg();
1301 if ( strval( $bits['index'] ) === '' ) {
1302 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
1303 $value = trim( $frame->expand( $bits['value'] ) );
1304 if ( preg_match( '/^(?:["\'](.+)["\']|""|\'\')$/s', $value, $m ) ) {
1305 $value = $m[1] ?? '';
1306 }
1307 $attributes[$name] = $value;
1308 }
1309 }
1310
1311 $stripList = $parser->getStripList();
1312 if ( !in_array( $tagName, $stripList ) ) {
1313 // we can't handle this tag (at least not now), so just re-emit it as an ordinary tag
1314 $attrText = '';
1315 foreach ( $attributes as $name => $value ) {
1316 $attrText .= ' ' . htmlspecialchars( $name ) .
1317 '="' . htmlspecialchars( $parser->killMarkers( $value ), ENT_COMPAT ) . '"';
1318 }
1319 if ( $inner === null ) {
1320 return "<$tagName$attrText/>";
1321 }
1322 return "<$tagName$attrText>$inner</$tagName>";
1323 }
1324
1325 $params = [
1326 'name' => $tagName,
1327 'inner' => $inner,
1328 'attributes' => $attributes,
1329 'close' => "</$tagName>",
1330 ];
1331 return $parser->extensionSubstitution( $params, $frame );
1332 }
1333
1347 private static function getCachedRevisionObject( $parser, $title, $vary ) {
1348 if ( !$title ) {
1349 return null;
1350 }
1351
1352 $revisionRecord = null;
1353
1354 $isSelfReferential = $title->equals( $parser->getTitle() );
1355 if ( $isSelfReferential ) {
1356 // Revision is for the same title that is currently being parsed. Only use the last
1357 // saved revision, regardless of Parser::getRevisionId() or fake revision injection
1358 // callbacks against the current title.
1359
1360 // FIXME (T318278): the above is the intention, but doesn't
1361 // describe the actual current behavior of this code, since
1362 // ->isCurrent() for the last saved revision will return
1363 // false so we're going to fall through and end up calling
1364 // ->getCurrentRevisionRecordOfTitle().
1365 $parserRevisionRecord = $parser->getRevisionRecordObject();
1366 if ( $parserRevisionRecord && $parserRevisionRecord->isCurrent() ) {
1367 $revisionRecord = $parserRevisionRecord;
1368 }
1369 }
1370
1371 $parserOutput = $parser->getOutput();
1372 if ( !$revisionRecord ) {
1373 if (
1374 !$parser->isCurrentRevisionOfTitleCached( $title ) &&
1376 ) {
1377 return null; // not allowed
1378 }
1379 // Get the current revision, ignoring Parser::getRevisionId() being null/old
1380 $revisionRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
1381 if ( !$revisionRecord ) {
1382 // Convert `false` error return to `null`
1383 $revisionRecord = null;
1384 }
1385 // Register dependency in templatelinks
1386 $parserOutput->addTemplate(
1387 $title,
1388 $revisionRecord ? $revisionRecord->getPageId() : 0,
1389 $revisionRecord ? $revisionRecord->getId() : 0
1390 );
1391 }
1392
1393 if ( $isSelfReferential ) {
1394 wfDebug( __METHOD__ . ": used current revision, setting {$vary->value}" );
1395 // Upon page save, the result of the parser function using this might change
1396 $parserOutput->setOutputFlag( $vary );
1397 if ( $vary === ParserOutputFlags::VARY_REVISION_SHA1 && $revisionRecord ) {
1398 try {
1399 $sha1 = $revisionRecord->getSha1();
1400 } catch ( RevisionAccessException ) {
1401 $sha1 = null;
1402 }
1403 $parserOutput->setRevisionUsedSha1Base36( $sha1 );
1404 }
1405 }
1406
1407 return $revisionRecord;
1408 }
1409
1417 public static function pageid( $parser, $title = null ) {
1418 $t = self::makeTitle( $parser, $title );
1419 if ( !$t ) {
1420 return '';
1421 } elseif ( !$t->canExist() || $t->isExternal() ) {
1422 return 0; // e.g. special page or interwiki link
1423 }
1424
1425 $parserOutput = $parser->getOutput();
1426
1427 if ( $t->equals( $parser->getTitle() ) ) {
1428 // Revision is for the same title that is currently being parsed.
1429 // Use the title from Parser in case a new page ID was injected into it.
1430 $parserOutput->setOutputFlag( ParserOutputFlags::VARY_PAGE_ID );
1431 $id = $parser->getTitle()->getArticleID();
1432 if ( $id ) {
1433 $parserOutput->setSpeculativePageIdUsed( $id );
1434 }
1435
1436 return $id;
1437 }
1438
1439 // Check the link cache for the title
1440 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1441 $pdbk = $t->getPrefixedDBkey();
1442 $id = $linkCache->getGoodLinkID( $pdbk );
1443 if ( $id != 0 || $linkCache->isBadLink( $pdbk ) ) {
1444 $parserOutput->addLink( $t, $id );
1445
1446 return $id;
1447 }
1448
1449 // We need to load it from the DB, so mark expensive
1450 if ( $parser->incrementExpensiveFunctionCount() ) {
1451 $id = $t->getArticleID();
1452 $parserOutput->addLink( $t, $id );
1453
1454 return $id;
1455 }
1456
1457 return null;
1458 }
1459
1467 public static function revisionid( $parser, $title = null ) {
1468 $t = self::makeTitle( $parser, $title );
1469 if ( $t === null || $t->isExternal() ) {
1470 return '';
1471 }
1472
1473 $services = MediaWikiServices::getInstance();
1474 if (
1475 $t->equals( $parser->getTitle() ) &&
1476 $services->getMainConfig()->get( MainConfigNames::MiserMode ) &&
1477 !$parser->getOptions()->getInterfaceMessage() &&
1478 // @TODO: disallow this word on all namespaces (T235957)
1479 $services->getNamespaceInfo()->isSubject( $t->getNamespace() )
1480 ) {
1481 // Use a stub result instead of the actual revision ID in order to avoid
1482 // double parses on page save but still allow preview detection (T137900)
1483 if ( $parser->getRevisionId() || $parser->getOptions()->getSpeculativeRevId() ) {
1484 return '-';
1485 } else {
1486 $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_EXISTS );
1487 return '';
1488 }
1489 }
1490 // Fetch revision from cache/database and return the value.
1491 // Inform the edit saving system that getting the canonical output
1492 // after revision insertion requires a parse that used that exact
1493 // revision ID.
1494 if ( $t->equals( $parser->getTitle() ) && $title === null ) {
1495 // special handling for no-arg case: use speculative rev id
1496 // for current page.
1497 $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_ID );
1498 $id = $parser->getRevisionId();
1499 if ( $id === 0 ) {
1500 $rev = $parser->getRevisionRecordObject();
1501 if ( $rev ) {
1502 $id = $rev->getId();
1503 }
1504 }
1505 if ( !$id ) {
1506 $id = $parser->getOptions()->getSpeculativeRevId();
1507 if ( $id ) {
1508 $parser->getOutput()->setSpeculativeRevIdUsed( $id );
1509 }
1510 }
1511 return (string)$id;
1512 }
1513 $rev = self::getCachedRevisionObject( $parser, $t, ParserOutputFlags::VARY_REVISION_ID );
1514 return $rev ? $rev->getId() : '';
1515 }
1516
1517 private static function getRevisionTimestampSubstring(
1518 Parser $parser,
1519 Title $title,
1520 int $start,
1521 int $len,
1522 int $mtts
1523 ): string {
1524 // If fetching the revision timestamp of the current page, substitute the
1525 // speculative timestamp to be used when this revision is saved. This
1526 // avoids having to invalidate the cache immediately by assuming the "last
1527 // saved revision" will in fact be this one.
1528 // Don't do this for interface messages (eg, edit notices) however; in that
1529 // case fall through and use the actual timestamp of the last saved revision.
1530 if ( $title->equals( $parser->getTitle() ) && !$parser->getOptions()->getInterfaceMessage() ) {
1531 // Get the timezone-adjusted timestamp to be used for this revision
1532 $resNow = substr( $parser->getRevisionTimestamp(), $start, $len );
1533 // Possibly set vary-revision if there is not yet an associated revision
1534 if ( !$parser->getRevisionRecordObject() ) {
1535 // Get the timezone-adjusted timestamp $mtts seconds in the future.
1536 // This future is relative to the current time and not that of the
1537 // parser options. The rendered timestamp can be compared to that
1538 // of the timestamp specified by the parser options.
1539 $resThen = substr(
1540 $parser->getContentLanguage()->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
1541 $start,
1542 $len
1543 );
1544
1545 if ( $resNow !== $resThen ) {
1546 // Inform the edit saving system that getting the canonical output after
1547 // revision insertion requires a parse that used an actual revision timestamp
1548 $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_TIMESTAMP );
1549 }
1550 }
1551
1552 return $resNow;
1553 } else {
1554 $rev = self::getCachedRevisionObject( $parser, $title, ParserOutputFlags::VARY_REVISION_TIMESTAMP );
1555 if ( !$rev ) {
1556 return '';
1557 }
1558 $resNow = substr(
1559 $parser->getContentLanguage()->userAdjust( $rev->getTimestamp(), '' ), $start, $len
1560 );
1561 return $resNow;
1562 }
1563 }
1564
1572 public static function revisionday( $parser, $title = null ) {
1573 $t = self::makeTitle( $parser, $title );
1574 if ( $t === null || $t->isExternal() ) {
1575 return '';
1576 }
1577 return strval( (int)self::getRevisionTimestampSubstring(
1578 $parser, $t, 6, 2, self::MAX_TTS
1579 ) );
1580 }
1581
1589 public static function revisionday2( $parser, $title = null ) {
1590 $t = self::makeTitle( $parser, $title );
1591 if ( $t === null || $t->isExternal() ) {
1592 return '';
1593 }
1594 return self::getRevisionTimestampSubstring(
1595 $parser, $t, 6, 2, self::MAX_TTS
1596 );
1597 }
1598
1606 public static function revisionmonth( $parser, $title = null ) {
1607 $t = self::makeTitle( $parser, $title );
1608 if ( $t === null || $t->isExternal() ) {
1609 return '';
1610 }
1611 return self::getRevisionTimestampSubstring(
1612 $parser, $t, 4, 2, self::MAX_TTS
1613 );
1614 }
1615
1623 public static function revisionmonth1( $parser, $title = null ) {
1624 $t = self::makeTitle( $parser, $title );
1625 if ( $t === null || $t->isExternal() ) {
1626 return '';
1627 }
1628 return strval( (int)self::getRevisionTimestampSubstring(
1629 $parser, $t, 4, 2, self::MAX_TTS
1630 ) );
1631 }
1632
1640 public static function revisionyear( $parser, $title = null ) {
1641 $t = self::makeTitle( $parser, $title );
1642 if ( $t === null || $t->isExternal() ) {
1643 return '';
1644 }
1645 return self::getRevisionTimestampSubstring(
1646 $parser, $t, 0, 4, self::MAX_TTS
1647 );
1648 }
1649
1657 public static function revisiontimestamp( $parser, $title = null ) {
1658 $t = self::makeTitle( $parser, $title );
1659 if ( $t === null || $t->isExternal() ) {
1660 return '';
1661 }
1662 return self::getRevisionTimestampSubstring(
1663 $parser, $t, 0, 14, self::MAX_TTS
1664 );
1665 }
1666
1674 public static function revisionuser( $parser, $title = null ) {
1675 $t = self::makeTitle( $parser, $title );
1676 if ( $t === null || $t->isExternal() ) {
1677 return '';
1678 }
1679 // VARY_USER informs the edit saving system that getting the canonical
1680 // output after revision insertion requires a parse that used the
1681 // actual user ID.
1682 if ( $t->equals( $parser->getTitle() ) ) {
1683 // Fall back to Parser's "revision user" for the current title
1684 $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_USER );
1685 // Note that getRevisionUser() can return null; we need to
1686 // be sure to cast this to (an empty) string, since returning
1687 // null means "magic variable not handled".
1688 return (string)$parser->getRevisionUser();
1689 }
1690 // Fetch revision from cache/database and return the value.
1691 $rev = self::getCachedRevisionObject( $parser, $t, ParserOutputFlags::VARY_USER );
1692 $user = ( $rev !== null ) ? $rev->getUser() : null;
1693 return $user ? $user->getName() : '';
1694 }
1695
1708 public static function cascadingsources( $parser, $title = '' ) {
1709 $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
1710 $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
1711 if ( $restrictionStore->areCascadeProtectionSourcesLoaded( $titleObject )
1713 ) {
1714 $names = [];
1715 $sources = $restrictionStore->getCascadeProtectionSources( $titleObject );
1716 $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
1717 foreach ( $sources[0] as $sourcePageIdentity ) {
1718 $names[] = $titleFormatter->getPrefixedText( $sourcePageIdentity );
1719 }
1720 return implode( '|', $names );
1721 }
1722 return '';
1723 }
1724
1725 public static function interwikilink( $parser, $prefix = '', $title = '', $linkText = null ) {
1726 $services = MediaWikiServices::getInstance();
1727 if (
1728 $prefix !== '' &&
1729 $services->getInterwikiLookup()->isValidInterwiki( $prefix )
1730 ) {
1731 if ( $linkText !== null ) {
1732 $linkText = Parser::stripOuterParagraph(
1733 # FIXME T382287: when using Parsoid this may leave
1734 # strip markers behind for embedded extension tags.
1735 $parser->recursiveTagParseFully( $linkText )
1736 );
1737 }
1738 [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
1739 $target = new TitleValue( NS_MAIN, $title, $frag, $prefix );
1740 $parser->getOutput()->addInterwikiLink( $target );
1741 return [
1742 'text' => Linker::link( $target, $linkText ),
1743 'isHTML' => true,
1744 ];
1745 }
1746 // Invalid interwiki link, render as plain text
1747 return [ 'found' => false ];
1748 }
1749
1750 public static function interlanguagelink( $parser, $prefix = '', $title = '', $linkText = null ) {
1751 $services = MediaWikiServices::getInstance();
1752 $extraInterlanguageLinkPrefixes = $services->getMainConfig()->get(
1753 MainConfigNames::ExtraInterlanguageLinkPrefixes
1754 );
1755 if (
1756 $prefix !== '' &&
1757 $services->getInterwikiLookup()->isValidInterwiki( $prefix ) &&
1758 (
1759 $services->getLanguageNameUtils()->getLanguageName(
1760 $prefix, LanguageNameUtils::AUTONYMS, LanguageNameUtils::DEFINED
1761 ) || in_array( $prefix, $extraInterlanguageLinkPrefixes, true )
1762 )
1763 ) {
1764 // $linkText is ignored for language links, but fragment is kept
1765 [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
1766 $parser->getOutput()->addLanguageLink(
1767 new TitleValue(
1768 NS_MAIN, $title, $frag, $prefix
1769 )
1770 );
1771 return '';
1772 }
1773 // Invalid language link, render as plain text
1774 return [ 'found' => false ];
1775 }
1776}
1777
1779class_alias( CoreParserFunctions::class, 'CoreParserFunctions' );
const NS_USER
Definition Defines.php:67
const NS_FILE
Definition Defines.php:71
const NS_MAIN
Definition Defines.php:65
const NS_SPECIAL
Definition Defines.php:54
const NS_MEDIA
Definition Defines.php:53
const PROTO_RELATIVE
Definition Defines.php:233
const NS_CATEGORY
Definition Defines.php:79
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
@phpcs-require-sorted-array
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:82
Category objects are immutable, strictly speaking.
Definition Category.php:44
A class for passing options to services.
Methods for dealing with language codes.
Base class for language-specific code.
Definition Language.php:81
Some internal bits split of from Skin.php.
Definition Linker.php:61
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:157
static plaintextParam( $plaintext)
Definition Message.php:1354
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 pagesinnamespace( $parser, $namespace=0, $raw=null)
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 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 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 padright( $parser, $string='', $length=0, $padding='0')
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 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 padleft( $parser, $string='', $length=0, $padding='0')
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 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:147
getTargetLanguageConverter()
Shorthand for getting a Language Converter for Target language.
Definition Parser.php:1666
markerSkipCallback( $s, callable $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:6373
tagNeedsNowikiStrippedInTagPF(string $lowerTagName)
Definition Parser.php:4025
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition Parser.php:1247
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:5135
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:6268
isCurrentRevisionOfTitleCached(LinkTarget $link)
Definition Parser.php:3635
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:6087
getContentLanguage()
Get the content language that this Parser is using.
Definition Parser.php:1257
parseWidthParam( $value, $parseHeight=true, bool $localized=false)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6421
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:6404
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:6182
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1193
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:4256
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1353
extensionSubstitution(array $params, PPFrame $frame, bool $processNowiki=false)
Return the text to be used for a given extension tag.
Definition Parser.php:4049
getRevisionRecordObject()
Get the revision record object for $this->mRevisionId.
Definition Parser.php:6097
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:6154
doQuotes( $text)
Helper function for handleAllQuotes()
Definition Parser.php:2000
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:899
fetchCurrentRevisionRecordOfTitle(LinkTarget $link)
Fetch the current revision of a given title as a RevisionRecord.
Definition Parser.php:3606
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:36
Parent class for all special pages.
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:78
inNamespace(int $ns)
Returns true if the title is inside the specified namespace.
Definition Title.php:1304
equals(object $other)
Compares with another Title.
Definition Title.php:3109
getDBkey()
Get the main part with underscores.
Definition Title.php:1037
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1019
getDefaultOption(string $opt, ?UserIdentity $userIdentity=null)
Get a given default option value.
User class for the MediaWiki software.
Definition User.php:123
hasFragment()
Whether the link target has a fragment.