MediaWiki master
CoreParserFunctions.php
Go to the documentation of this file.
1<?php
10namespace MediaWiki\Parser;
11
12use InvalidArgumentException;
28use Wikimedia\Bcp47Code\Bcp47CodeValue;
30use Wikimedia\RemexHtml\Tokenizer\Attributes;
31use Wikimedia\RemexHtml\Tokenizer\PlainAttributes;
32use Wikimedia\Timestamp\TimestampFormat as TS;
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 'isbn',
120 ];
121 foreach ( $noHashFunctions as $func ) {
122 $parser->setFunctionHook( $func, self::$func( ... ), Parser::SFH_NO_HASH );
123 }
124
125 $parser->setFunctionHook( 'int', self::intFunction( ... ), Parser::SFH_NO_HASH );
126 $parser->setFunctionHook( 'special', self::special( ... ) );
127 $parser->setFunctionHook( 'speciale', self::speciale( ... ) );
128 $parser->setFunctionHook( 'tag', self::tagObj( ... ), Parser::SFH_OBJECT_ARGS );
129 $parser->setFunctionHook( 'formatdate', self::formatDate( ... ) );
130
131 if ( $allowDisplayTitle ) {
132 $parser->setFunctionHook(
133 'displaytitle',
134 self::displaytitle( ... ),
136 );
137 }
138 if ( $allowSlowParserFunctions ) {
139 $parser->setFunctionHook(
140 'pagesinnamespace',
141 self::pagesinnamespace( ... ),
143 );
144 }
145 }
146
153 public static function intFunction( $parser, $part1 = '', ...$params ) {
154 if ( strval( $part1 ) !== '' ) {
155 $message = wfMessage( $part1, $params )
156 ->inLanguage( $parser->getOptions()->getUserLangObj() );
157 return [ $message->plain(), 'noparse' => false ];
158 } else {
159 return [ 'found' => false ];
160 }
161 }
162
170 public static function formatDate( $parser, $date, $defaultPref = null ) {
171 $lang = $parser->getTargetLanguage();
172 $df = MediaWikiServices::getInstance()->getDateFormatterFactory()->get( $lang );
173
174 $date = trim( $date );
175
176 $pref = $parser->getOptions()->getDateFormat();
177
178 // Specify a different default date format other than the normal default
179 // if the user has 'default' for their setting
180 if ( $pref == 'default' && $defaultPref ) {
181 $pref = $defaultPref;
182 }
183
184 $date = $df->reformat( $pref, $date, [ 'match-whole' ] );
185 return $date;
186 }
187
193 public static function ns( $parser, $part1 = '' ) {
194 if ( intval( $part1 ) || $part1 == "0" ) {
195 $index = intval( $part1 );
196 } else {
197 $index = $parser->getContentLanguage()->getNsIndex( str_replace( ' ', '_', $part1 ) );
198 }
199 if ( $index !== false ) {
200 return $parser->getContentLanguage()->getFormattedNsText( $index );
201 } else {
202 return [ 'found' => false ];
203 }
204 }
205
211 public static function nse( $parser, $part1 = '' ) {
212 $ret = self::ns( $parser, $part1 );
213 if ( is_string( $ret ) ) {
214 $ret = wfUrlencode( str_replace( ' ', '_', $ret ) );
215 }
216 return $ret;
217 }
218
231 public static function urlencode( $parser, $s = '', $arg = null ) {
232 static $magicWords = null;
233 if ( $magicWords === null ) {
235 $parser->getMagicWordFactory()->newArray( [ 'url_path', 'url_query', 'url_wiki' ] );
236 }
237 switch ( $magicWords->matchStartToEnd( $arg ?? '' ) ) {
238 // Encode as though it's a wiki page, '_' for ' '.
239 case 'url_wiki':
240 $func = wfUrlencode( ... );
241 $s = str_replace( ' ', '_', $s );
242 break;
243
244 // Encode for an HTTP Path, '%20' for ' '.
245 case 'url_path':
246 $func = rawurlencode( ... );
247 break;
248
249 // Encode for HTTP query, '+' for ' '.
250 case 'url_query':
251 default:
252 $func = urlencode( ... );
253 }
254 // See T105242, where the choice to kill markers and various
255 // other options were discussed.
256 return $func( $parser->killMarkers( $s ) );
257 }
258
264 public static function lcfirst( $parser, $s = '' ) {
265 return $parser->getContentLanguage()->lcfirst( $s );
266 }
267
273 public static function ucfirst( $parser, $s = '' ) {
274 return $parser->getContentLanguage()->ucfirst( $s );
275 }
276
282 public static function lc( $parser, $s = '' ) {
283 return $parser->markerSkipCallback( $s, $parser->getContentLanguage()->lc( ... ) );
284 }
285
291 public static function uc( $parser, $s = '' ) {
292 return $parser->markerSkipCallback( $s, $parser->getContentLanguage()->uc( ... ) );
293 }
294
301 public static function localurl( $parser, $s = '', $arg = null ) {
302 return self::urlFunction( 'getLocalURL', $s, $arg );
303 }
304
311 public static function localurle( $parser, $s = '', $arg = null ) {
312 $temp = self::urlFunction( 'getLocalURL', $s, $arg );
313 if ( !is_string( $temp ) ) {
314 return $temp;
315 } else {
316 return htmlspecialchars( $temp, ENT_COMPAT );
317 }
318 }
319
326 public static function fullurl( $parser, $s = '', $arg = null ) {
327 return self::urlFunction( 'getFullURL', $s, $arg );
328 }
329
336 public static function fullurle( $parser, $s = '', $arg = null ) {
337 $temp = self::urlFunction( 'getFullURL', $s, $arg );
338 if ( !is_string( $temp ) ) {
339 return $temp;
340 } else {
341 return htmlspecialchars( $temp, ENT_COMPAT );
342 }
343 }
344
351 public static function canonicalurl( $parser, $s = '', $arg = null ) {
352 return self::urlFunction( 'getCanonicalURL', $s, $arg );
353 }
354
361 public static function canonicalurle( $parser, $s = '', $arg = null ) {
362 $temp = self::urlFunction( 'getCanonicalURL', $s, $arg );
363 if ( !is_string( $temp ) ) {
364 return $temp;
365 } else {
366 return htmlspecialchars( $temp, ENT_COMPAT );
367 }
368 }
369
376 public static function urlFunction( $func, $s = '', $arg = null ) {
377 # Due to order of execution of a lot of bits, the values might be encoded
378 # before arriving here; if that's true, then the title can't be created
379 # and the variable will fail. If we can't get a decent title from the first
380 # attempt, url-decode and try for a second.
381 $title = Title::newFromText( $s ) ?? Title::newFromURL( urldecode( $s ) );
382 if ( $title !== null ) {
383 # Convert NS_MEDIA -> NS_FILE
384 if ( $title->inNamespace( NS_MEDIA ) ) {
385 $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
386 }
387 if ( $arg !== null ) {
388 $text = $title->$func( $arg );
389 } else {
390 $text = $title->$func();
391 }
392 return $text;
393 } else {
394 return [ 'found' => false ];
395 }
396 }
397
405 public static function formatnum( $parser, $num = '', $arg1 = '', $arg2 = '' ) {
406 static $magicWords = null;
407 if ( $magicWords === null ) {
408 $magicWords = $parser->getMagicWordFactory()->newArray( [
409 'rawsuffix',
410 'nocommafysuffix',
411 'lossless',
412 ] );
413 }
414
415 $modifiers = [ $magicWords->matchStartToEnd( $arg1 ), $magicWords->matchStartToEnd( $arg2 ) ];
416 $targetLanguage = $parser->getTargetLanguage();
417 if ( in_array( 'rawsuffix', $modifiers, true ) ) {
418 $func = [ $targetLanguage, 'parseFormattedNumber' ];
419 } else {
420 if ( in_array( 'nocommafysuffix', $modifiers, true ) ) {
421 $func = [ $targetLanguage, 'formatNumNoSeparators' ];
422 } else {
423 $func = [ $targetLanguage, 'formatNum' ];
424 $func = self::getLegacyFormatNum( $parser, $func );
425 }
426 if ( in_array( 'lossless', $modifiers, true ) ) {
427 $potentiallyLossyFunc = $func;
428 $func = static function ( $num ) use ( $targetLanguage, $potentiallyLossyFunc ) {
429 $formatted = $potentiallyLossyFunc( $num );
430 $parsed = $targetLanguage->parseFormattedNumber( $formatted );
431 if ( $num === $parsed ) {
432 return $formatted;
433 } else {
434 return (string)$num;
435 }
436 };
437 }
438 }
439 return $parser->markerSkipCallback( $num, $func );
440 }
441
448 private static function getLegacyFormatNum( $parser, $callback ) {
449 // For historic reasons, the formatNum parser function will
450 // take arguments which are not actually formatted numbers,
451 // which then trigger deprecation warnings in Language::formatNum*.
452 // Instead emit a tracking category instead to allow linting.
453 return static function ( $number ) use ( $parser, $callback ) {
454 $validNumberRe = '(-(?=[\d\.]))?(\d+|(?=\.\d))(\.\d*)?([Ee][-+]?\d+)?';
455 if (
456 !is_numeric( $number ) &&
457 $number !== 'NAN' &&
458 $number !== 'INF' &&
459 $number !== '-INF'
460 ) {
461 $parser->addTrackingCategory( 'nonnumeric-formatnum' );
462 // Don't split on NAN/INF in the legacy case since they are
463 // likely to be found embedded inside non-numeric text.
464 return preg_replace_callback( "/{$validNumberRe}/", static function ( $m ) use ( $callback ) {
465 return $callback( $m[0] );
466 }, $number );
467 }
468 return $callback( $number );
469 };
470 }
471
478 public static function grammar( $parser, $case = '', $word = '' ) {
479 $word = $parser->killMarkers( $word );
480 return $parser->getTargetLanguage()->convertGrammar( $word, $case );
481 }
482
489 public static function gender( $parser, $username, ...$forms ) {
490 // Some shortcuts to avoid loading user data unnecessarily
491 if ( count( $forms ) === 0 ) {
492 return '';
493 } elseif ( count( $forms ) === 1 ) {
494 return $forms[0];
495 }
496
497 $username = trim( $username );
498
499 $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
500 $gender = $userOptionsLookup->getDefaultOption( 'gender' );
501
502 // allow prefix and normalize (e.g. "&#42;foo" -> "*foo" ).
503 $title = Title::newFromText( $username, NS_USER );
504
505 if ( $title && $title->inNamespace( NS_USER ) ) {
506 $username = $title->getText();
507 }
508
509 // check parameter, or use the ParserOptions if in interface message
510 $user = User::newFromName( $username );
511 $genderCache = MediaWikiServices::getInstance()->getGenderCache();
512 if ( $user ) {
513 $gender = $genderCache->getGenderOf( $user, __METHOD__ );
514 } elseif ( $username === '' && $parser->getOptions()->isMessage() ) {
515 $gender = $genderCache->getGenderOf( $parser->getOptions()->getUserIdentity(), __METHOD__ );
516 }
517 $ret = $parser->getTargetLanguage()->gender( $gender, $forms );
518 return $ret;
519 }
520
527 public static function plural( $parser, $text = '', ...$forms ) {
528 $text = $parser->getTargetLanguage()->parseFormattedNumber( $text );
529 settype( $text, ctype_digit( $text ) ? 'int' : 'float' );
530 // @phan-suppress-next-line PhanTypeMismatchArgument Phan does not handle settype
531 return $parser->getTargetLanguage()->convertPlural( $text, $forms );
532 }
533
534 public static function formal( Parser $parser, string ...$forms ): string {
535 $index = $parser->getTargetLanguage()->getFormalityIndex();
536 return $forms[$index] ?? $forms[0];
537 }
538
544 public static function bidi( $parser, $text = '' ) {
545 return $parser->getTargetLanguage()->embedBidi( $text );
546 }
547
557 public static function displaytitle( $parser, $text = '', $uarg = '' ) {
558 $restrictDisplayTitle = MediaWikiServices::getInstance()->getMainConfig()
560
561 static $magicWords = null;
562 if ( $magicWords === null ) {
563 $magicWords = $parser->getMagicWordFactory()->newArray(
564 [ 'displaytitle_noerror', 'displaytitle_noreplace' ] );
565 }
566 $arg = $magicWords->matchStartToEnd( $uarg );
567
568 // parse a limited subset of wiki markup (just the single quote items)
569 $text = $parser->doQuotes( $text );
570
571 // remove stripped text (e.g. the UNIQ-QINU stuff) that was generated by tag extensions/whatever
572 $text = $parser->killMarkers( $text );
573
574 // See T28547 for rationale for this processing.
575 // list of disallowed tags for DISPLAYTITLE
576 // these will be escaped even though they are allowed in normal wiki text
577 $bad = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'blockquote', 'ol', 'ul', 'li', 'hr',
578 'table', 'tr', 'th', 'td', 'dl', 'dd', 'caption', 'p', 'ruby', 'rb', 'rt', 'rtc', 'rp', 'br' ];
579
580 // disallow some styles that could be used to bypass $wgRestrictDisplayTitle
581 if ( $restrictDisplayTitle ) {
582 // This code is tested with the cases marked T28547 in
583 // parserTests.txt
584 $htmlTagsCallback = static function ( Attributes $attr ): Attributes {
585 $decoded = $attr->getValues();
586
587 if ( isset( $decoded['style'] ) ) {
588 // this is called later anyway, but we need it right now for the regexes below to be safe
589 // calling it twice doesn't hurt
590 $decoded['style'] = Sanitizer::checkCss( $decoded['style'] );
591
592 if ( preg_match( '/(display|user-select|visibility)\s*:/i', $decoded['style'] ) ) {
593 $decoded['style'] = '/* attempt to bypass $wgRestrictDisplayTitle */';
594 }
595 }
596
597 return new PlainAttributes( $decoded );
598 };
599 } else {
600 $htmlTagsCallback = null;
601 }
602
603 // only requested titles that normalize to the actual title are allowed through
604 // if $wgRestrictDisplayTitle is true (it is by default)
605 // mimic the escaping process that occurs in OutputPage::setPageTitle
606 $text = Sanitizer::removeSomeTags( $text, [
607 'attrCallback' => $htmlTagsCallback,
608 'removeTags' => $bad,
609 ] );
610 $title = Title::newFromText( Sanitizer::stripAllTags( $text ) );
611 // Decode entities in $text the same way that Title::newFromText does
612 $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
613
614 if ( !$restrictDisplayTitle ||
615 ( $title instanceof Title
616 && !$title->hasFragment()
617 && $title->equals( $parser->getTitle() ) )
618 ) {
619 $old = $parser->getOutput()->getPageProperty( 'displaytitle' );
620 if ( $old === null || $arg !== 'displaytitle_noreplace' ) {
621 $parser->getOutput()->setDisplayTitle( $text );
622 }
623 if ( $old !== null && $old !== $text && !$arg ) {
624
625 $converter = $parser->getTargetLanguageConverter();
626 return '<span class="error">' .
627 $parser->msg( 'duplicate-displaytitle',
628 // Message should be parsed, but these params should only be escaped.
629 $converter->markNoConversion( wfEscapeWikiText( $old ) ),
630 $converter->markNoConversion( wfEscapeWikiText( $filteredText ) )
631 )->text() .
632 '</span>';
633 } else {
634 return '';
635 }
636 } else {
637 $parser->getOutput()->addWarningMsg(
638 'restricted-displaytitle',
639 // Message should be parsed, but this param should only be escaped.
640 wfEscapeWikiText( $filteredText )
641 );
642 $parser->addTrackingCategory( 'restricted-displaytitle-ignored' );
643 }
644 }
645
655 private static function matchAgainstMagicword(
656 MagicWordFactory $magicWordFactory, $magicword, $value
657 ) {
658 $value = trim( strval( $value ) );
659 if ( $value === '' ) {
660 return false;
661 }
662 $mwObject = $magicWordFactory->get( $magicword );
663 return $mwObject->matchStartToEnd( $value );
664 }
665
675 public static function formatRaw(
676 $num, $raw, $language, ?MagicWordFactory $magicWordFactory = null
677 ) {
678 if ( $raw !== null && $raw !== '' ) {
679 if ( !$magicWordFactory ) {
680 $magicWordFactory = MediaWikiServices::getInstance()->getMagicWordFactory();
681 }
682 if ( self::matchAgainstMagicword( $magicWordFactory, 'rawsuffix', $raw ) ) {
683 return (string)$num;
684 }
685 }
686 return $language->formatNum( $num );
687 }
688
694 public static function numberofpages( $parser, $raw = null ) {
695 return self::formatRaw( SiteStats::pages(), $raw, $parser->getTargetLanguage() );
696 }
697
703 public static function numberofusers( $parser, $raw = null ) {
704 return self::formatRaw( SiteStats::users(), $raw, $parser->getTargetLanguage() );
705 }
706
712 public static function numberofactiveusers( $parser, $raw = null ) {
713 return self::formatRaw( SiteStats::activeUsers(), $raw, $parser->getTargetLanguage() );
714 }
715
721 public static function numberofarticles( $parser, $raw = null ) {
722 return self::formatRaw( SiteStats::articles(), $raw, $parser->getTargetLanguage() );
723 }
724
730 public static function numberoffiles( $parser, $raw = null ) {
731 return self::formatRaw( SiteStats::images(), $raw, $parser->getTargetLanguage() );
732 }
733
739 public static function numberofadmins( $parser, $raw = null ) {
740 return self::formatRaw(
741 SiteStats::numberingroup( 'sysop' ),
742 $raw,
743 $parser->getTargetLanguage()
744 );
745 }
746
752 public static function numberofedits( $parser, $raw = null ) {
753 return self::formatRaw( SiteStats::edits(), $raw, $parser->getTargetLanguage() );
754 }
755
762 public static function pagesinnamespace( $parser, $namespace = '0', $raw = null ) {
763 return self::formatRaw(
764 SiteStats::pagesInNs( intval( $namespace ) ),
765 $raw,
766 $parser->getTargetLanguage()
767 );
768 }
769
776 public static function numberingroup( $parser, $name = '', $raw = null ) {
777 return self::formatRaw(
778 SiteStats::numberingroup( strtolower( $name ) ),
779 $raw,
780 $parser->getTargetLanguage()
781 );
782 }
783
792 private static function makeTitle( Parser $parser, ?string $t ) {
793 if ( $t === null ) {
794 // For consistency with magic variable forms
795 $title = $parser->getTitle();
796 } else {
797 $title = Title::newFromText( $t );
798 }
799 return $title;
800 }
801
810 public static function namespace( $parser, $title = null ) {
811 $t = self::makeTitle( $parser, $title );
812 if ( $t === null ) {
813 return '';
814 }
815 return str_replace( '_', ' ', $t->getNsText() );
816 }
817
823 public static function namespacee( $parser, $title = null ) {
824 $t = self::makeTitle( $parser, $title );
825 if ( $t === null ) {
826 return '';
827 }
828 return wfUrlencode( $t->getNsText() );
829 }
830
836 public static function namespacenumber( $parser, $title = null ) {
837 $t = self::makeTitle( $parser, $title );
838 if ( $t === null ) {
839 return '';
840 }
841 return (string)$t->getNamespace();
842 }
843
849 public static function talkspace( $parser, $title = null ) {
850 $t = self::makeTitle( $parser, $title );
851 if ( $t === null || !$t->canHaveTalkPage() ) {
852 return '';
853 }
854 return str_replace( '_', ' ', $t->getTalkNsText() );
855 }
856
862 public static function talkspacee( $parser, $title = null ) {
863 $t = self::makeTitle( $parser, $title );
864 if ( $t === null || !$t->canHaveTalkPage() ) {
865 return '';
866 }
867 return wfUrlencode( $t->getTalkNsText() );
868 }
869
875 public static function subjectspace( $parser, $title = null ) {
876 $t = self::makeTitle( $parser, $title );
877 if ( $t === null ) {
878 return '';
879 }
880 return str_replace( '_', ' ', $t->getSubjectNsText() );
881 }
882
888 public static function subjectspacee( $parser, $title = null ) {
889 $t = self::makeTitle( $parser, $title );
890 if ( $t === null ) {
891 return '';
892 }
893 return wfUrlencode( $t->getSubjectNsText() );
894 }
895
903 public static function pagename( $parser, $title = null ) {
904 $t = self::makeTitle( $parser, $title );
905 if ( $t === null ) {
906 return '';
907 }
908 return wfEscapeWikiText( $t->getText() );
909 }
910
916 public static function pagenamee( $parser, $title = null ) {
917 $t = self::makeTitle( $parser, $title );
918 if ( $t === null ) {
919 return '';
920 }
921 return wfEscapeWikiText( $t->getPartialURL() );
922 }
923
929 public static function fullpagename( $parser, $title = null ) {
930 $t = self::makeTitle( $parser, $title );
931 if ( $t === null ) {
932 return '';
933 }
934 return wfEscapeWikiText( $t->getPrefixedText() );
935 }
936
942 public static function fullpagenamee( $parser, $title = null ) {
943 $t = self::makeTitle( $parser, $title );
944 if ( $t === null ) {
945 return '';
946 }
947 return wfEscapeWikiText( $t->getPrefixedURL() );
948 }
949
955 public static function subpagename( $parser, $title = null ) {
956 $t = self::makeTitle( $parser, $title );
957 if ( $t === null ) {
958 return '';
959 }
960 return wfEscapeWikiText( $t->getSubpageText() );
961 }
962
968 public static function subpagenamee( $parser, $title = null ) {
969 $t = self::makeTitle( $parser, $title );
970 if ( $t === null ) {
971 return '';
972 }
973 return wfEscapeWikiText( $t->getSubpageUrlForm() );
974 }
975
981 public static function rootpagename( $parser, $title = null ) {
982 $t = self::makeTitle( $parser, $title );
983 if ( $t === null ) {
984 return '';
985 }
986 return wfEscapeWikiText( $t->getRootText() );
987 }
988
994 public static function rootpagenamee( $parser, $title = null ) {
995 $t = self::makeTitle( $parser, $title );
996 if ( $t === null ) {
997 return '';
998 }
999 return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getRootText() ) ) );
1000 }
1001
1007 public static function basepagename( $parser, $title = null ) {
1008 $t = self::makeTitle( $parser, $title );
1009 if ( $t === null ) {
1010 return '';
1011 }
1012 return wfEscapeWikiText( $t->getBaseText() );
1013 }
1014
1020 public static function basepagenamee( $parser, $title = null ) {
1021 $t = self::makeTitle( $parser, $title );
1022 if ( $t === null ) {
1023 return '';
1024 }
1025 return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getBaseText() ) ) );
1026 }
1027
1033 public static function talkpagename( $parser, $title = null ) {
1034 $t = self::makeTitle( $parser, $title );
1035 if ( $t === null || !$t->canHaveTalkPage() ) {
1036 return '';
1037 }
1038 return wfEscapeWikiText( $t->getTalkPageIfDefined()->getPrefixedText() );
1039 }
1040
1046 public static function talkpagenamee( $parser, $title = null ) {
1047 $t = self::makeTitle( $parser, $title );
1048 if ( $t === null || !$t->canHaveTalkPage() ) {
1049 return '';
1050 }
1051 return wfEscapeWikiText( $t->getTalkPageIfDefined()->getPrefixedURL() );
1052 }
1053
1059 public static function subjectpagename( $parser, $title = null ) {
1060 $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1061 $t = self::makeTitle( $parser, $title );
1062 if ( $t === null ) {
1063 return '';
1064 }
1065 return wfEscapeWikiText( Title::newFromLinkTarget( $namespaceInfo->getSubjectPage( $t ) )->getPrefixedText() );
1066 }
1067
1073 public static function subjectpagenamee( $parser, $title = null ) {
1074 $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1075 $t = self::makeTitle( $parser, $title );
1076 if ( $t === null ) {
1077 return '';
1078 }
1079 return wfEscapeWikiText( Title::newFromLinkTarget( $namespaceInfo->getSubjectPage( $t ) )->getPrefixedURL() );
1080 }
1081
1092 public static function pagesincategory( $parser, $name = '', $arg1 = '', $arg2 = '' ) {
1093 static $magicWords = null;
1094 if ( $magicWords === null ) {
1095 $magicWords = $parser->getMagicWordFactory()->newArray( [
1096 'pagesincategory_all',
1097 'pagesincategory_pages',
1098 'pagesincategory_subcats',
1099 'pagesincategory_files'
1100 ] );
1101 }
1102 static $cache = [];
1103
1104 // split the given option to its variable
1105 if ( self::matchAgainstMagicword( $parser->getMagicWordFactory(), 'rawsuffix', $arg1 ) ) {
1106 // {{pagesincategory:|raw[|type]}}
1107 $raw = $arg1;
1108 $type = $magicWords->matchStartToEnd( $arg2 );
1109 } else {
1110 // {{pagesincategory:[|type[|raw]]}}
1111 $type = $magicWords->matchStartToEnd( $arg1 );
1112 $raw = $arg2;
1113 }
1114 if ( !$type ) { // backward compatibility
1115 $type = 'pagesincategory_all';
1116 }
1117
1118 $title = Title::makeTitleSafe( NS_CATEGORY, $name );
1119 if ( !$title ) { # invalid title
1120 return self::formatRaw( 0, $raw, $parser->getTargetLanguage() );
1121 }
1122 $languageConverter = MediaWikiServices::getInstance()
1123 ->getLanguageConverterFactory()
1124 ->getLanguageConverter( $parser->getContentLanguage() );
1125 $languageConverter->findVariantLink( $name, $title, true );
1126
1127 // Normalize name for cache
1128 $name = $title->getDBkey();
1129
1130 if ( !isset( $cache[$name] ) ) {
1131 $category = Category::newFromTitle( $title );
1132
1133 $allCount = $subcatCount = $fileCount = $pageCount = 0;
1134 if ( $parser->incrementExpensiveFunctionCount() ) {
1135 $allCount = $category->getMemberCount();
1136 $subcatCount = $category->getSubcatCount();
1137 $fileCount = $category->getFileCount();
1138 $pageCount = $category->getPageCount( Category::COUNT_CONTENT_PAGES );
1139 }
1140 $cache[$name]['pagesincategory_all'] = $allCount;
1141 $cache[$name]['pagesincategory_pages'] = $pageCount;
1142 $cache[$name]['pagesincategory_subcats'] = $subcatCount;
1143 $cache[$name]['pagesincategory_files'] = $fileCount;
1144 }
1145
1146 $count = $cache[$name][$type];
1147 return self::formatRaw( $count, $raw, $parser->getTargetLanguage() );
1148 }
1149
1159 public static function pagesize( $parser, $page = '', $raw = null ) {
1160 $title = Title::newFromText( $page );
1161
1162 if ( !is_object( $title ) || $title->isExternal() ) {
1163 return self::formatRaw( 0, $raw, $parser->getTargetLanguage() );
1164 }
1165
1166 // fetch revision from cache/database and return the value
1167 $rev = self::getCachedRevisionObject( $parser, $title, ParserOutputFlags::VARY_REVISION_SHA1 );
1168 $length = $rev ? $rev->getSize() : 0;
1169 if ( $length === null ) {
1170 // We've had bugs where rev_len was not being recorded for empty pages, see T135414
1171 $length = 0;
1172 }
1173 return self::formatRaw( $length, $raw, $parser->getTargetLanguage() );
1174 }
1175
1188 public static function protectionlevel( $parser, $type = '', $title = '' ) {
1189 $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
1190 $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
1191 if ( $restrictionStore->areRestrictionsLoaded( $titleObject ) || $parser->incrementExpensiveFunctionCount() ) {
1192 $restrictions = $restrictionStore->getRestrictions( $titleObject, strtolower( $type ) );
1193 # RestrictionStore::getRestrictions returns an array, its possible it may have
1194 # multiple values in the future
1195 return implode( ',', $restrictions );
1196 }
1197 return '';
1198 }
1199
1212 public static function protectionexpiry( $parser, $type = '', $title = '' ) {
1213 $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
1214 $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
1215 if ( $restrictionStore->areRestrictionsLoaded( $titleObject ) || $parser->incrementExpensiveFunctionCount() ) {
1216 // getRestrictionExpiry() returns null on invalid type; trying to
1217 // match protectionlevel() function that returns empty string instead
1218 return $restrictionStore->getRestrictionExpiry( $titleObject, strtolower( $type ) ) ?? '';
1219 }
1220 return '';
1221 }
1222
1231 public static function language( $parser, $code = '', $inLanguage = '' ) {
1232 if ( $code === '' ) {
1233 $code = $parser->getTargetLanguage()->getCode();
1234 }
1235 if ( $inLanguage === '' ) {
1236 $inLanguage = LanguageNameUtils::AUTONYMS;
1237 }
1239 ->getLanguageNameUtils()
1240 ->getLanguageName( $code, $inLanguage );
1241 return $lang !== '' ? $lang : LanguageCode::bcp47( $code );
1242 }
1243
1255 public static function dir( Parser $parser, string $code = '', string $arg = '' ): string {
1256 static $magicWords = null;
1257 $languageFactory = MediaWikiServices::getInstance()->getLanguageFactory();
1258
1259 if ( $code === '' ) {
1260 $lang = $parser->getTargetLanguage();
1261 } else {
1262 if ( $arg !== '' ) {
1263 if ( $magicWords === null ) {
1264 $magicWords = $parser->getMagicWordFactory()->newArray( [ 'language_option_bcp47' ] );
1265 }
1266 if ( $magicWords->matchStartToEnd( $arg ) === 'language_option_bcp47' ) {
1267 // Prefer the BCP-47 interpretation of this code.
1268 $code = new Bcp47CodeValue( $code );
1269 }
1270 }
1271 try {
1272 $lang = $languageFactory->getLanguage( $code );
1273 } catch ( InvalidArgumentException ) {
1274 $parser->addTrackingCategory( 'bad-language-code-category' );
1275 return 'ltr';
1276 }
1277 }
1278 return $lang->getDir();
1279 }
1280
1289 public static function bcp47( Parser $parser, string $code = '' ): string {
1290 if ( $code === '' ) {
1291 return $parser->getTargetLanguage()->toBcp47Code();
1292 } else {
1293 return LanguageCode::bcp47( $code );
1294 }
1295 }
1296
1306 public static function pad(
1307 $parser, $string, $length, $padding = '0', $direction = STR_PAD_RIGHT
1308 ) {
1309 $padding = $parser->killMarkers( $padding );
1310 $lengthOfPadding = mb_strlen( $padding );
1311 if ( $lengthOfPadding == 0 ) {
1312 return $string;
1313 }
1314
1315 # The remaining length to add counts down to 0 as padding is added
1316 $length = min( (int)$length, 500 ) - mb_strlen( $string );
1317 if ( $length <= 0 ) {
1318 // Nothing to add
1319 return $string;
1320 }
1321
1322 # $finalPadding is just $padding repeated enough times so that
1323 # mb_strlen( $string ) + mb_strlen( $finalPadding ) == $length
1324 $finalPadding = '';
1325 while ( $length > 0 ) {
1326 # If $length < $lengthofPadding, truncate $padding so we get the
1327 # exact length desired.
1328 $finalPadding .= mb_substr( $padding, 0, $length );
1329 $length -= $lengthOfPadding;
1330 }
1331
1332 if ( $direction == STR_PAD_LEFT ) {
1333 return $finalPadding . $string;
1334 } else {
1335 return $string . $finalPadding;
1336 }
1337 }
1338
1346 public static function padleft( $parser, $string = '', $length = '0', $padding = '0' ) {
1347 return self::pad( $parser, $string, $length, $padding, STR_PAD_LEFT );
1348 }
1349
1357 public static function padright( $parser, $string = '', $length = '0', $padding = '0' ) {
1358 return self::pad( $parser, $string, $length, $padding );
1359 }
1360
1366 public static function anchorencode( $parser, $text ) {
1367 $text = $parser->killMarkers( $text );
1368 $section = substr( $parser->guessSectionNameFromWikiText( $text ), 1 );
1369 $encodedSection = Sanitizer::safeEncodeAttribute( $section );
1370 // decode underscores to avoid breaking templates (T407131)
1371 return str_replace( '&#95;', '_', $encodedSection );
1372 }
1373
1379 public static function special( $parser, $text ) {
1380 [ $page, $subpage ] = MediaWikiServices::getInstance()->getSpecialPageFactory()->
1381 resolveAlias( $text );
1382 if ( $page ) {
1383 $title = SpecialPage::getTitleFor( $page, $subpage );
1384 return $title->getPrefixedText();
1385 } else {
1386 // unknown special page, just use the given text as its title, if at all possible
1387 $title = Title::makeTitleSafe( NS_SPECIAL, $text );
1388 return $title ? $title->getPrefixedText() : self::special( $parser, 'Badtitle' );
1389 }
1390 }
1391
1397 public static function speciale( $parser, $text ) {
1398 return wfUrlencode( str_replace( ' ', '_', self::special( $parser, $text ) ) );
1399 }
1400
1409 public static function defaultsort( $parser, $text, $uarg = '' ) {
1410 static $magicWords = null;
1411 if ( $magicWords === null ) {
1412 $magicWords = $parser->getMagicWordFactory()->newArray(
1413 [ 'defaultsort_noerror', 'defaultsort_noreplace' ] );
1414 }
1415 $arg = $magicWords->matchStartToEnd( $uarg );
1416
1417 $text = trim( $text );
1418 if ( $text === '' ) {
1419 return '';
1420 }
1421 $old = $parser->getOutput()->getPageProperty( 'defaultsort' );
1422 if ( $old === null || $arg !== 'defaultsort_noreplace' ) {
1423 $parser->getOutput()->setPageProperty( 'defaultsort', $text );
1424 }
1425
1426 if ( $old === null || $old == $text || $arg ) {
1427 return '';
1428 } else {
1429 $converter = $parser->getTargetLanguageConverter();
1430 return '<span class="error">' .
1431 $parser->msg( 'duplicate-defaultsort',
1432 // Message should be parsed, but these params should only be escaped.
1433 $converter->markNoConversion( wfEscapeWikiText( $old ) ),
1434 $converter->markNoConversion( wfEscapeWikiText( $text ) )
1435 )->text() .
1436 '</span>';
1437 }
1438 }
1439
1451 public static function filepath( $parser, $name = '', $argA = '', $argB = '' ) {
1452 $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $name );
1453
1454 if ( $argA == 'nowiki' ) {
1455 // {{filepath: | option [| size] }}
1456 $isNowiki = true;
1457 $parsedWidthParam = $parser->parseWidthParam( $argB );
1458 } else {
1459 // {{filepath: [| size [|option]] }}
1460 $parsedWidthParam = $parser->parseWidthParam( $argA );
1461 $isNowiki = ( $argB == 'nowiki' );
1462 }
1463
1464 if ( $file ) {
1465 $url = $file->getFullUrl();
1466
1467 // If a size is requested...
1468 if ( count( $parsedWidthParam ) ) {
1469 $mto = $file->transform( $parsedWidthParam );
1470 // ... and we can
1471 if ( $mto && !$mto->isError() ) {
1472 // ... change the URL to point to a thumbnail.
1473 $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
1474 $url = $urlUtils->expand( $mto->getUrl(), PROTO_RELATIVE ) ?? false;
1475 }
1476 }
1477 if ( $isNowiki ) {
1478 return [ $url, 'nowiki' => true ];
1479 }
1480 return $url;
1481 } else {
1482 return '';
1483 }
1484 }
1485
1493 public static function tagObj( $parser, $frame, $args ) {
1494 if ( !count( $args ) ) {
1495 return '';
1496 }
1497 $tagName = strtolower( trim( $parser->killMarkers(
1498 $frame->expand( array_shift( $args ) )
1499 ) ) );
1500
1501 if ( count( $args ) ) {
1502 $inner = $frame->expand( array_shift( $args ) );
1503 if ( $parser->tagNeedsNowikiStrippedInTagPF( $tagName ) ) {
1504 // This is the T299103 workaround for <syntaxhighlight>,
1505 // and reproduces the code in SyntaxHighlight::parserHook.
1506 // The Parsoid extension API (SyntaxHighlight::sourceToDom)
1507 // doesn't (yet) know about strip state, and so can't do
1508 // this itself.
1509 $inner = $parser->getStripState()->replaceNoWikis(
1510 $inner,
1511 static function ( string $text, ?string $contentSrc ) {
1512 // Depending on the output type, $text here could be
1513 // the xml serialization of the nowiki tag or the
1514 // nowiki content processed to html, which escapes
1515 // certain characters. By contrast, $contentSrc
1516 // is the source of the inner content
1517 if ( $contentSrc === null ) {
1518 // Some extensions (SyntaxHighlight, TemplateStyles)
1519 // are known to wrap their output in a nowiki strip
1520 // marker to skip the bulk of Parser::internalParseHalfParsed
1521 // but that in turn is wrapped in a general marker
1522 // and isn't expected to show up here
1523 wfLogWarning( "Unexpected null contentSrc for text: " . $text );
1524 }
1525 return ( $contentSrc ?? '' );
1526 }
1527 );
1528 }
1529 } else {
1530 $inner = null;
1531 }
1532
1533 $attributes = [];
1534 foreach ( $args as $arg ) {
1535 $bits = $arg->splitArg();
1536 if ( strval( $bits['index'] ) === '' ) {
1537 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
1538 $value = trim( $frame->expand( $bits['value'] ) );
1539 if ( preg_match( '/^(?:["\'](.+)["\']|""|\'\')$/s', $value, $m ) ) {
1540 $value = $m[1] ?? '';
1541 }
1542 $attributes[$name] = $value;
1543 }
1544 }
1545
1546 $stripList = $parser->getStripList();
1547 if ( !in_array( $tagName, $stripList ) ) {
1548 // we can't handle this tag (at least not now), so just re-emit it as an ordinary tag
1549 $attrText = '';
1550 foreach ( $attributes as $name => $value ) {
1551 $attrText .= ' ' . htmlspecialchars( $name ) .
1552 '="' . htmlspecialchars( $parser->killMarkers( $value ), ENT_COMPAT ) . '"';
1553 }
1554 if ( $inner === null ) {
1555 return "<$tagName$attrText/>";
1556 }
1557 return "<$tagName$attrText>$inner</$tagName>";
1558 }
1559
1560 $params = [
1561 'name' => $tagName,
1562 'inner' => $inner,
1563 'attributes' => $attributes,
1564 'close' => "</$tagName>",
1565 ];
1566 return $parser->extensionSubstitution( $params, $frame );
1567 }
1568
1582 private static function getCachedRevisionObject( $parser, $title, $vary ) {
1583 if ( !$title ) {
1584 return null;
1585 }
1586
1587 $revisionRecord = null;
1588
1589 $isSelfReferential = $title->equals( $parser->getTitle() );
1590 if ( $isSelfReferential ) {
1591 // Revision is for the same title that is currently being parsed. Only use the last
1592 // saved revision, regardless of Parser::getRevisionId() or fake revision injection
1593 // callbacks against the current title.
1594
1595 // FIXME (T318278): the above is the intention, but doesn't
1596 // describe the actual current behavior of this code, since
1597 // ->isCurrent() for the last saved revision will return
1598 // false so we're going to fall through and end up calling
1599 // ->fetchCurrentRevisionRecordOfTitle().
1600 $parserRevisionRecord = $parser->getRevisionRecordObject();
1601 if ( $parserRevisionRecord && $parserRevisionRecord->isCurrent() ) {
1602 $revisionRecord = $parserRevisionRecord;
1603 }
1604 }
1605
1606 $parserOutput = $parser->getOutput();
1607 if ( !$revisionRecord ) {
1608 if (
1609 !$parser->isCurrentRevisionOfTitleCached( $title ) &&
1611 ) {
1612 return null; // not allowed
1613 }
1614 // Get the current revision, ignoring Parser::getRevisionId() being null/old
1615 $revisionRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
1616 if ( !$revisionRecord ) {
1617 // Convert `false` error return to `null`
1618 $revisionRecord = null;
1619 }
1620 // Register dependency in templatelinks
1621 $parserOutput->addTemplate(
1622 $title,
1623 $revisionRecord ? $revisionRecord->getPageId() : 0,
1624 $revisionRecord ? $revisionRecord->getId() : 0
1625 );
1626 }
1627
1628 if ( $isSelfReferential ) {
1629 wfDebug( __METHOD__ . ": used current revision, setting {$vary->value}" );
1630 // Upon page save, the result of the parser function using this might change
1631 $parserOutput->setOutputFlag( $vary );
1632 if ( $vary === ParserOutputFlags::VARY_REVISION_SHA1 && $revisionRecord ) {
1633 try {
1634 $sha1 = $revisionRecord->getSha1();
1635 } catch ( RevisionAccessException ) {
1636 $sha1 = null;
1637 }
1638 $parserOutput->setRevisionUsedSha1Base36( $sha1 );
1639 }
1640 }
1641
1642 return $revisionRecord;
1643 }
1644
1652 public static function pageid( $parser, $title = null ) {
1653 $t = self::makeTitle( $parser, $title );
1654 if ( !$t ) {
1655 return '';
1656 } elseif ( !$t->canExist() || $t->isExternal() ) {
1657 return 0; // e.g. special page or interwiki link
1658 }
1659
1660 $parserOutput = $parser->getOutput();
1661
1662 if ( $t->equals( $parser->getTitle() ) ) {
1663 // Revision is for the same title that is currently being parsed.
1664 // Use the title from Parser in case a new page ID was injected into it.
1665 $parserOutput->setOutputFlag( ParserOutputFlags::VARY_PAGE_ID );
1666 $id = $parser->getTitle()->getArticleID();
1667 if ( $id ) {
1668 $parserOutput->setSpeculativePageIdUsed( $id );
1669 }
1670
1671 return $id;
1672 }
1673
1674 // Check the link cache for the title
1675 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1676 $pdbk = $t->getPrefixedDBkey();
1677 $id = $linkCache->getGoodLinkID( $pdbk );
1678 if ( $id != 0 || $linkCache->isBadLink( $pdbk ) ) {
1679 $parserOutput->addLink( $t, $id );
1680
1681 return $id;
1682 }
1683
1684 // We need to load it from the DB, so mark expensive
1685 if ( $parser->incrementExpensiveFunctionCount() ) {
1686 $id = $t->getArticleID();
1687 $parserOutput->addLink( $t, $id );
1688
1689 return $id;
1690 }
1691
1692 return null;
1693 }
1694
1702 public static function revisionid( $parser, $title = null ) {
1703 $t = self::makeTitle( $parser, $title );
1704 if ( $t === null || $t->isExternal() ) {
1705 return '';
1706 }
1707
1708 if (
1709 $t->equals( $parser->getTitle() ) &&
1710 $parser->getMiserMode() &&
1711 !$parser->getOptions()->getInterfaceMessage() &&
1712 // @TODO: disallow this word on all namespaces (T235957)
1713 $parser->getNamespaceInfo()->isSubject( $t->getNamespace() )
1714 ) {
1715 // Use a stub result instead of the actual revision ID in order to avoid
1716 // double parses on page save but still allow preview detection (T137900)
1717 if ( $parser->getRevisionId() || $parser->getOptions()->getSpeculativeRevId() ) {
1718 return '-';
1719 } else {
1720 $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_EXISTS );
1721 return '';
1722 }
1723 }
1724 // Fetch revision from cache/database and return the value.
1725 // Inform the edit saving system that getting the canonical output
1726 // after revision insertion requires a parse that used that exact
1727 // revision ID.
1728 if ( $t->equals( $parser->getTitle() ) && $title === null ) {
1729 // special handling for no-arg case: use speculative rev id
1730 // for current page.
1731 $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_ID );
1732 $id = $parser->getRevisionId();
1733 if ( $id === 0 ) {
1734 $rev = $parser->getRevisionRecordObject();
1735 if ( $rev ) {
1736 $id = $rev->getId();
1737 }
1738 }
1739 if ( !$id ) {
1740 $id = $parser->getOptions()->getSpeculativeRevId();
1741 if ( $id ) {
1742 $parser->getOutput()->setSpeculativeRevIdUsed( $id );
1743 }
1744 }
1745 return (string)$id;
1746 }
1747 $rev = self::getCachedRevisionObject( $parser, $t, ParserOutputFlags::VARY_REVISION_ID );
1748 return $rev ? $rev->getId() : '';
1749 }
1750
1751 private static function getRevisionTimestampSubstring(
1752 Parser $parser,
1753 Title $title,
1754 int $start,
1755 int $len,
1756 int $mtts
1757 ): string {
1758 // If fetching the revision timestamp of the current page, substitute the
1759 // speculative timestamp to be used when this revision is saved. This
1760 // avoids having to invalidate the cache immediately by assuming the "last
1761 // saved revision" will in fact be this one.
1762 // Don't do this for interface messages (eg, edit notices) however; in that
1763 // case fall through and use the actual timestamp of the last saved revision.
1764 if ( $title->equals( $parser->getTitle() ) && !$parser->getOptions()->getInterfaceMessage() ) {
1765 // Get the timezone-adjusted timestamp to be used for this revision
1766 $resNow = substr( $parser->getRevisionTimestamp(), $start, $len );
1767 // Possibly set vary-revision if there is not yet an associated revision
1768 if ( !$parser->getRevisionRecordObject() ) {
1769 // Get the timezone-adjusted timestamp $mtts seconds in the future.
1770 // This future is relative to the current time and not that of the
1771 // parser options. The rendered timestamp can be compared to that
1772 // of the timestamp specified by the parser options.
1773 $resThen = substr(
1774 $parser->getContentLanguage()->userAdjust( wfTimestamp( TS::MW, time() + $mtts ), '' ),
1775 $start,
1776 $len
1777 );
1778
1779 if ( $resNow !== $resThen ) {
1780 // Inform the edit saving system that getting the canonical output after
1781 // revision insertion requires a parse that used an actual revision timestamp
1782 $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_TIMESTAMP );
1783 }
1784 }
1785
1786 return $resNow;
1787 } else {
1788 $rev = self::getCachedRevisionObject( $parser, $title, ParserOutputFlags::VARY_REVISION_TIMESTAMP );
1789 if ( !$rev ) {
1790 return '';
1791 }
1792 $resNow = substr(
1793 $parser->getContentLanguage()->userAdjust( $rev->getTimestamp(), '' ), $start, $len
1794 );
1795 return $resNow;
1796 }
1797 }
1798
1806 public static function revisionday( $parser, $title = null ) {
1807 $t = self::makeTitle( $parser, $title );
1808 if ( $t === null || $t->isExternal() ) {
1809 return '';
1810 }
1811 return strval( (int)self::getRevisionTimestampSubstring(
1812 $parser, $t, 6, 2, self::MAX_TTS
1813 ) );
1814 }
1815
1823 public static function revisionday2( $parser, $title = null ) {
1824 $t = self::makeTitle( $parser, $title );
1825 if ( $t === null || $t->isExternal() ) {
1826 return '';
1827 }
1828 return self::getRevisionTimestampSubstring(
1829 $parser, $t, 6, 2, self::MAX_TTS
1830 );
1831 }
1832
1840 public static function revisionmonth( $parser, $title = null ) {
1841 $t = self::makeTitle( $parser, $title );
1842 if ( $t === null || $t->isExternal() ) {
1843 return '';
1844 }
1845 return self::getRevisionTimestampSubstring(
1846 $parser, $t, 4, 2, self::MAX_TTS
1847 );
1848 }
1849
1857 public static function revisionmonth1( $parser, $title = null ) {
1858 $t = self::makeTitle( $parser, $title );
1859 if ( $t === null || $t->isExternal() ) {
1860 return '';
1861 }
1862 return strval( (int)self::getRevisionTimestampSubstring(
1863 $parser, $t, 4, 2, self::MAX_TTS
1864 ) );
1865 }
1866
1874 public static function revisionyear( $parser, $title = null ) {
1875 $t = self::makeTitle( $parser, $title );
1876 if ( $t === null || $t->isExternal() ) {
1877 return '';
1878 }
1879 return self::getRevisionTimestampSubstring(
1880 $parser, $t, 0, 4, self::MAX_TTS
1881 );
1882 }
1883
1891 public static function revisiontimestamp( $parser, $title = null ) {
1892 $t = self::makeTitle( $parser, $title );
1893 if ( $t === null || $t->isExternal() ) {
1894 return '';
1895 }
1896 return self::getRevisionTimestampSubstring(
1897 $parser, $t, 0, 14, self::MAX_TTS
1898 );
1899 }
1900
1908 public static function revisionuser( $parser, $title = null ) {
1909 $t = self::makeTitle( $parser, $title );
1910 if ( $t === null || $t->isExternal() ) {
1911 return '';
1912 }
1913 // VARY_USER informs the edit saving system that getting the canonical
1914 // output after revision insertion requires a parse that used the
1915 // actual user ID.
1916 if ( $t->equals( $parser->getTitle() ) ) {
1917 // Fall back to Parser's "revision user" for the current title
1918 $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_USER );
1919 // Note that getRevisionUser() can return null; we need to
1920 // be sure to cast this to (an empty) string, since returning
1921 // null means "magic variable not handled".
1922 return (string)$parser->getRevisionUser();
1923 }
1924 // Fetch revision from cache/database and return the value.
1925 $rev = self::getCachedRevisionObject( $parser, $t, ParserOutputFlags::VARY_USER );
1926 $user = ( $rev !== null ) ? $rev->getUser() : null;
1927 return $user ? $user->getName() : '';
1928 }
1929
1942 public static function cascadingsources( $parser, $title = '' ) {
1943 $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
1944 $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
1945 if ( $restrictionStore->areCascadeProtectionSourcesLoaded( $titleObject )
1947 ) {
1948 $names = [];
1949 $sources = $restrictionStore->getCascadeProtectionSources( $titleObject );
1950 $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
1951 foreach ( $sources[0] as $sourcePageIdentity ) {
1952 $names[] = $titleFormatter->getPrefixedText( $sourcePageIdentity );
1953 }
1954 return implode( '|', $names );
1955 }
1956 return '';
1957 }
1958
1966 public static function contentmodel( Parser $parser, ?string $format = null, ?string $title = null ) {
1967 static $magicWords = null;
1968 if ( $magicWords === null ) {
1969 $magicWords = $parser->getMagicWordFactory()->newArray( [
1970 'contentmodel_canonical',
1971 'contentmodel_local',
1972 ] );
1973 }
1974
1975 $formatType = $format === null ? 'contentmodel_local' :
1976 $magicWords->matchStartToEnd( $format );
1977
1978 $t = self::makeTitle( $parser, $title );
1979 if ( $t === null ) {
1980 return '';
1981 }
1982
1983 if ( !$parser->incrementExpensiveFunctionCount() ) {
1984 return '';
1985 }
1986
1987 $contentModel = $t->getContentModel();
1988 if ( $formatType === 'contentmodel_canonical' ) {
1989 return wfEscapeWikiText( $contentModel );
1990 } elseif ( $formatType === 'contentmodel_local' ) {
1991 $localizedContentModel = ContentHandler::getLocalizedName( $contentModel, $parser->getTargetLanguage() );
1992 return wfEscapeWikiText( $localizedContentModel );
1993 } else {
1994 // Unknown format option
1995 return '';
1996 }
1997 }
1998
2004 public static function isbn( $parser, $isbn = '' ) {
2005 $space = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
2006 $spdash = "(?:-|$space)"; # a dash or a non-newline space
2007 $regex = "!
2008 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
2009 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
2010 [0-9Xx] # check digit
2011 !xu";
2012 if ( !preg_match( $regex, $isbn ) ) {
2013 return [ 'found' => false ];
2014 }
2015 $isbn = preg_replace( "/$space/", ' ', $isbn );
2016 $num = strtr( $isbn, [
2017 '-' => '',
2018 ' ' => '',
2019 'x' => 'X',
2020 ] );
2021 $target = SpecialPage::getTitleFor( 'Booksources', $num );
2022 $parser->getOutput()->addLink( $target );
2023 return [
2024 'text' => $parser->getLinkRenderer()->makeKnownLink(
2025 $target,
2026 "ISBN $isbn",
2027 [
2028 'class' => 'internal mw-magiclink-isbn',
2029 'title' => false, // suppress title attribute
2030 ]
2031 ),
2032 'isHTML' => true,
2033 ];
2034 }
2035
2043 public static function interwikilink( $parser, $prefix = '', $title = '', $linkText = null ) {
2044 $services = MediaWikiServices::getInstance();
2045 if (
2046 $prefix !== '' &&
2047 $services->getInterwikiLookup()->isValidInterwiki( $prefix )
2048 ) {
2049
2050 $target = self::getTitleValueSafe( $title, $prefix );
2051
2052 if ( $target !== null ) {
2053 $displayText = null;
2054 if ( $linkText !== null ) {
2055 $linkText = Parser::stripOuterParagraph(
2056 # FIXME T382287: when using Parsoid this may leave
2057 # strip markers behind for embedded extension tags.
2058 $parser->recursiveTagParseFully( $linkText )
2059 );
2060 $displayText = new HTMLArmor( $linkText );
2061 }
2062 $parser->getOutput()->addInterwikiLink( $target );
2063 return [
2064 'text' => $services->getLinkRenderer()->makeLink(
2065 $target,
2066 $displayText
2067 ),
2068 'isHTML' => true,
2069 ];
2070 }
2071 }
2072 // Invalid interwiki link, render as plain text
2073 return [ 'found' => false ];
2074 }
2075
2083 public static function interlanguagelink( $parser, $prefix = '', $title = '', $linkText = null ) {
2084 $services = MediaWikiServices::getInstance();
2085 $extraInterlanguageLinkPrefixes = $services->getMainConfig()->get(
2086 MainConfigNames::ExtraInterlanguageLinkPrefixes
2087 );
2088 if (
2089 $prefix !== '' &&
2090 $services->getInterwikiLookup()->isValidInterwiki( $prefix ) &&
2091 (
2092 $services->getLanguageNameUtils()->getLanguageName(
2093 $prefix, LanguageNameUtils::AUTONYMS, LanguageNameUtils::DEFINED
2094 ) || in_array( $prefix, $extraInterlanguageLinkPrefixes, true )
2095 )
2096 ) {
2097 // $linkText is ignored for language links, but fragment is kept
2098 $target = self::getTitleValueSafe( $title, $prefix );
2099
2100 if ( $target !== null ) {
2101 $parser->getOutput()->addLanguageLink( $target );
2102 return '';
2103 }
2104 }
2105 // Invalid language link, render as plain text
2106 return [ 'found' => false ];
2107 }
2108
2124 private static function getTitleValueSafe( $title, $prefix ): ?TitleValue {
2125 [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
2126
2127 try {
2128 return new TitleValue( NS_MAIN, $title, $frag, $prefix );
2129 } catch ( InvalidArgumentException ) {
2130 return null;
2131 }
2132 }
2133}
2134
2136class_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,...
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
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:71
Category objects are immutable, strictly speaking.
Definition Category.php:30
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.
A service that provides utilities to do with language names and codes.
Base class for language-specific code.
Definition Language.php:65
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.
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:138
getTargetLanguageConverter()
Shorthand for getting a Language Converter for Target language.
Definition Parser.php:1552
markerSkipCallback( $s, callable $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:6338
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition Parser.php:1119
tagNeedsNowikiStrippedInTagPF(string $lowerTagName)
Definition Parser.php:3972
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition Parser.php:1134
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:5097
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:6231
isCurrentRevisionOfTitleCached(LinkTarget $link)
Definition Parser.php:3531
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:6068
getContentLanguage()
Get the content language that this Parser is using.
Definition Parser.php:1144
parseWidthParam( $value, $parseHeight=true, bool $localized=false)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6386
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:6369
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:6163
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1080
extensionSubstitution(array $params, PPFrame $frame)
Return the text to be used for a given extension tag.
Definition Parser.php:3992
msg( $key,... $params)
Helper function to correctly set the target language and title of a message based on the parser conte...
Definition Parser.php:4212
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1240
getRevisionRecordObject()
Get the revision record object for $this->mRevisionId.
Definition Parser.php:6078
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:6135
doQuotes( $text)
Helper function for handleAllQuotes()
Definition Parser.php:1917
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:827
fetchCurrentRevisionRecordOfTitle(LinkTarget $link)
Fetch the current revision of a given title as a RevisionRecord.
Definition Parser.php:3502
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:21
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:1292
equals(object $other)
Compares with another Title.
Definition Title.php:3089
getDBkey()
Get the main part with underscores.
Definition Title.php:1025
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1007
getDefaultOption(string $opt, ?UserIdentity $userIdentity=null)
Get a given default option value.
User class for the MediaWiki software.
Definition User.php:129
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:18
hasFragment()
Whether the link target has a fragment.