Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 449
ParserFunctions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 27
20592
0.00% covered (danger)
0.00%
0 / 449
 registerClearHook
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 8
 getExprParser
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 expr
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
 ifexpr
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 17
 if
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 7
 ifeq
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 8
 iferror
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 18
 switch
0.00% covered (danger)
0.00%
0 / 1
156
0.00% covered (danger)
0.00%
0 / 45
 rel2abs
0.00% covered (danger)
0.00%
0 / 1
132
0.00% covered (danger)
0.00%
0 / 35
 ifexistInternal
0.00% covered (danger)
0.00%
0 / 1
182
0.00% covered (danger)
0.00%
0 / 47
 ifexist
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 10
 timeCommon
0.00% covered (danger)
0.00%
0 / 1
380
0.00% covered (danger)
0.00%
0 / 78
 time
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 6
 localTime
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 5
 titleparts
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 21
 checkLength
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 tooLongError
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 4
 runLen
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 runPos
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 15
 runRPos
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 15
 runSub
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 11
 runCount
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 12
 runReplace
0.00% covered (danger)
0.00%
0 / 1
110
0.00% covered (danger)
0.00%
0 / 34
 runExplode
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 22
 runUrlDecode
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
 decodeTrimExpand
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 4
 getLanguageConverter
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 4
<?php
namespace MediaWiki\Extension\ParserFunctions;
use DateTime;
use DateTimeZone;
use Exception;
use ILanguageConverter;
use Language;
use MediaWiki\MediaWikiServices;
use MWTimestamp;
use Parser;
use PPFrame;
use PPNode;
use Sanitizer;
use StringUtils;
use StubObject;
use Title;
/**
 * Parser function handlers
 *
 * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions
 */
class ParserFunctions {
    private static $mExprParser;
    private static $mTimeCache = [];
    private static $mTimeChars = 0;
    /** ~10 seconds */
    private const MAX_TIME_CHARS = 6000;
    /**
     * Register ParserClearState hook.
     * We defer this until needed to avoid the loading of the code of this file
     * when no parser function is actually called.
     */
    private static function registerClearHook() {
        static $done = false;
        if ( !$done ) {
            global $wgHooks;
            $wgHooks['ParserClearState'][] = static function () {
                self::$mTimeChars = 0;
            };
            $done = true;
        }
    }
    /**
     * @return ExprParser
     */
    private static function &getExprParser() {
        if ( !isset( self::$mExprParser ) ) {
            self::$mExprParser = new ExprParser;
        }
        return self::$mExprParser;
    }
    /**
     * {{#expr: expression }}
     *
     * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##expr
     *
     * @param Parser $parser
     * @param string $expr
     * @return string
     */
    public static function expr( Parser $parser, $expr = '' ) {
        try {
            return self::getExprParser()->doExpression( $expr );
        } catch ( ExprError $e ) {
            return '<strong class="error">' . htmlspecialchars( $e->getUserFriendlyMessage() ) . '</strong>';
        }
    }
    /**
     * {{#ifexpr: expression | value if true | value if false }}
     *
     * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##ifexpr
     *
     * @param Parser $parser
     * @param PPFrame $frame
     * @param PPNode[] $args
     * @return string
     */
    public static function ifexpr( Parser $parser, PPFrame $frame, array $args ) {
        $expr = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : '';
        $then = $args[1] ?? '';
        $else = $args[2] ?? '';
        try {
            $result = self::getExprParser()->doExpression( $expr );
            if ( is_numeric( $result ) ) {
                $result = (float)$result;
            }
            $result = $result ? $then : $else;
        } catch ( ExprError $e ) {
            return '<strong class="error">' . htmlspecialchars( $e->getUserFriendlyMessage() ) . '</strong>';
        }
        if ( is_object( $result ) ) {
            $result = trim( $frame->expand( $result ) );
        }
        return $result;
    }
    /**
     * {{#if: test string | value if test string is not empty | value if test string is empty }}
     *
     * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##if
     *
     * @param Parser $parser
     * @param PPFrame $frame
     * @param PPNode[] $args
     * @return string
     */
    public static function if( Parser $parser, PPFrame $frame, array $args ) {
        $test = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : '';
        if ( $test !== '' ) {
            return isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : '';
        } else {
            return isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : '';
        }
    }
    /**
     * {{#ifeq: string 1 | string 2 | value if identical | value if different }}
     *
     * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##ifeq
     *
     * @param Parser $parser
     * @param PPFrame $frame
     * @param PPNode[] $args
     * @return string
     */
    public static function ifeq( Parser $parser, PPFrame $frame, array $args ) {
        $left = isset( $args[0] ) ? self::decodeTrimExpand( $args[0], $frame ) : '';
        $right = isset( $args[1] ) ? self::decodeTrimExpand( $args[1], $frame ) : '';
        // Strict compare is not possible here. 01 should equal 1 for example.
        /** @noinspection TypeUnsafeComparisonInspection */
        if ( $left == $right ) {
            return isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : '';
        } else {
            return isset( $args[3] ) ? trim( $frame->expand( $args[3] ) ) : '';
        }
    }
    /**
     * {{#iferror: test string | value if error | value if no error }}
     *
     * Error is when the input string contains an HTML object with class="error", as
     * generated by other parser functions such as #expr, #time and #rel2abs.
     *
     * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##iferror
     *
     * @param Parser $parser
     * @param PPFrame $frame
     * @param PPNode[] $args
     * @return string
     */
    public static function iferror( Parser $parser, PPFrame $frame, array $args ) {
        $test = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : '';
        $then = $args[1] ?? false;
        $else = $args[2] ?? false;
        if ( preg_match(
            '/<(?:strong|span|p|div)\s(?:[^\s>]*\s+)*?class="(?:[^"\s>]*\s+)*?error(?:\s[^">]*)?"/',
            $test )
        ) {
            $result = $then;
        } elseif ( $else === false ) {
            $result = $test;
        } else {
            $result = $else;
        }
        if ( $result === false ) {
            return '';
        }
        return trim( $frame->expand( $result ) );
    }
    /**
     * {{#switch: comparison string
     * | case = result
     * | case = result
     * | ...
     * | default result
     * }}
     *
     * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##switch
     *
     * @param Parser $parser
     * @param PPFrame $frame
     * @param PPNode[] $args
     * @return string
     */
    public static function switch( Parser $parser, PPFrame $frame, array $args ) {
        if ( count( $args ) === 0 ) {
            return '';
        }
        $primary = self::decodeTrimExpand( array_shift( $args ), $frame );
        $found = $defaultFound = false;
        $default = null;
        $lastItemHadNoEquals = false;
        $lastItem = '';
        $mwDefault = $parser->getMagicWordFactory()->get( 'default' );
        foreach ( $args as $arg ) {
            $bits = $arg->splitArg();
            $nameNode = $bits['name'];
            $index = $bits['index'];
            $valueNode = $bits['value'];
            if ( $index === '' ) {
                # Found "="
                $lastItemHadNoEquals = false;
                if ( $found ) {
                    # Multiple input match
                    return trim( $frame->expand( $valueNode ) );
                } else {
                    $test = self::decodeTrimExpand( $nameNode, $frame );
                    /** @noinspection TypeUnsafeComparisonInspection */
                    if ( $test == $primary ) {
                        # Found a match, return now
                        return trim( $frame->expand( $valueNode ) );
                    } elseif ( $defaultFound || $mwDefault->matchStartToEnd( $test ) ) {
                        $default = $valueNode;
                        $defaultFound = false;
                    } # else wrong case, continue
                }
            } else {
                # Multiple input, single output
                # If the value matches, set a flag and continue
                $lastItemHadNoEquals = true;
                // $lastItem is an "out" variable
                $decodedTest = self::decodeTrimExpand( $valueNode, $frame, $lastItem );
                /** @noinspection TypeUnsafeComparisonInspection */
                if ( $decodedTest == $primary ) {
                    $found = true;
                } elseif ( $mwDefault->matchStartToEnd( $decodedTest ) ) {
                    $defaultFound = true;
                }
            }
        }
        # Default case
        # Check if the last item had no = sign, thus specifying the default case
        if ( $lastItemHadNoEquals ) {
            return $lastItem;
        } elseif ( $default !== null ) {
            return trim( $frame->expand( $default ) );
        } else {
            return '';
        }
    }
    /**
     * {{#rel2abs: path }} or {{#rel2abs: path | base path }}
     *
     * Returns the absolute path to a subpage, relative to the current article
     * title. Treats titles as slash-separated paths.
     *
     * Following subpage link syntax instead of standard path syntax, an
     * initial slash is treated as a relative path, and vice versa.
     *
     * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##rel2abs
     *
     * @param Parser $parser
     * @param string $to
     * @param string $from
     *
     * @return string
     */
    public static function rel2abs( Parser $parser, $to = '', $from = '' ) {
        $from = trim( $from );
        if ( $from === '' ) {
            $from = $parser->getTitle()->getPrefixedText();
        }
        $to = rtrim( $to, ' /' );
        // if we have an empty path, or just one containing a dot
        if ( $to === '' || $to === '.' ) {
            return $from;
        }
        // if the path isn't relative
        if ( substr( $to, 0, 1 ) !== '/' &&
            substr( $to, 0, 2 ) !== './' &&
            substr( $to, 0, 3 ) !== '../' &&
            $to !== '..'
        ) {
            $from = '';
        }
        // Make a long path, containing both, enclose it in /.../
        $fullPath = '/' . $from . '/' . $to . '/';
        // remove redundant current path dots
        $fullPath = preg_replace( '!/(\./)+!', '/', $fullPath );
        // remove double slashes
        $fullPath = preg_replace( '!/{2,}!', '/', $fullPath );
        // remove the enclosing slashes now
        $fullPath = trim( $fullPath, '/' );
        $exploded = explode( '/', $fullPath );
        $newExploded = [];
        foreach ( $exploded as $current ) {
            if ( $current === '..' ) { // removing one level
                if ( !count( $newExploded ) ) {
                    // attempted to access a node above root node
                    $msg = wfMessage( 'pfunc_rel2abs_invalid_depth', $fullPath )
                        ->inContentLanguage()->escaped();
                    return '<strong class="error">' . $msg . '</strong>';
                }
                // remove last level from the stack
                array_pop( $newExploded );
            } else {
                // add the current level to the stack
                $newExploded[] = $current;
            }
        }
        // we can now join it again
        return implode( '/', $newExploded );
    }
    /**
     * @param Parser $parser
     * @param PPFrame $frame
     * @param string $titletext
     * @param PPNode|string|null $then
     * @param PPNode|string|null $else
     *
     * @return PPNode|string|null
     */
    private static function ifexistInternal(
        Parser $parser, PPFrame $frame, $titletext = '', $then = '', $else = ''
    ) {
        $title = Title::newFromText( $titletext );
        self::getLanguageConverter( $parser->getContentLanguage() )
            ->findVariantLink( $titletext, $title, true );
        if ( $title ) {
            if ( $title->getNamespace() === NS_MEDIA ) {
                /* If namespace is specified as NS_MEDIA, then we want to
                 * check the physical file, not the "description" page.
                 */
                if ( !$parser->incrementExpensiveFunctionCount() ) {
                    return $else;
                }
                $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title );
                if ( !$file ) {
                    $parser->getOutput()->addImage(
                        $title->getDBKey(), false, false );
                    return $else;
                }
                $parser->getOutput()->addImage(
                    $file->getName(), $file->getTimestamp(), $file->getSha1() );
                return $file->exists() ? $then : $else;
            } elseif ( $title->isSpecialPage() ) {
                /* Don't bother with the count for special pages,
                 * since their existence can be checked without
                 * accessing the database.
                 */
                return MediaWikiServices::getInstance()->getSpecialPageFactory()
                    ->exists( $title->getDBkey() ) ? $then : $else;
            } elseif ( $title->isExternal() ) {
                /* Can't check the existence of pages on other sites,
                 * so just return $else.  Makes a sort of sense, since
                 * they don't exist _locally_.
                 */
                return $else;
            } else {
                $pdbk = $title->getPrefixedDBkey();
                $lc = MediaWikiServices::getInstance()->getLinkCache();
                $id = $lc->getGoodLinkID( $pdbk );
                if ( $id !== 0 ) {
                    $parser->getOutput()->addLink( $title, $id );
                    return $then;
                } elseif ( $lc->isBadLink( $pdbk ) ) {
                    $parser->getOutput()->addLink( $title, 0 );
                    return $else;
                }
                if ( !$parser->incrementExpensiveFunctionCount() ) {
                    return $else;
                }
                $id = $title->getArticleID();
                $parser->getOutput()->addLink( $title, $id );
                // bug 70495: don't just check whether the ID != 0
                if ( $title->exists() ) {
                    return $then;
                }
            }
        }
        return $else;
    }
    /**
     * {{#ifexist: page title | value if exists | value if doesn't exist }}
     *
     * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##ifexist
     *
     * @param Parser $parser
     * @param PPFrame $frame
     * @param PPNode[] $args
     * @return string
     */
    public static function ifexist( Parser $parser, PPFrame $frame, array $args ) {
        $title = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : '';
        $then = $args[1] ?? null;
        $else = $args[2] ?? null;
        $result = self::ifexistInternal( $parser, $frame, $title, $then, $else );
        if ( $result === null ) {
            return '';
        } else {
            return trim( $frame->expand( $result ) );
        }
    }
    /**
     * Used by time() and localTime()
     *
     * @param Parser $parser
     * @param PPFrame|null $frame
     * @param string $format
     * @param string $date
     * @param string $language
     * @param string|bool $local
     * @return string
     */
    private static function timeCommon(
        Parser $parser, PPFrame $frame = null, $format = '', $date = '', $language = '', $local = false
    ) {
        global $wgLocaltimezone;
        self::registerClearHook();
        if ( $date === '' ) {
            $cacheKey = $parser->getOptions()->getTimestamp();
            $timestamp = new MWTimestamp( $cacheKey );
            $date = $timestamp->getTimestamp( TS_ISO_8601 );
            $useTTL = true;
        } else {
            $cacheKey = $date;
            $useTTL = false;
        }
        if ( isset( self::$mTimeCache[$format][$cacheKey][$language][$local] ) ) {
            $cachedVal = self::$mTimeCache[$format][$cacheKey][$language][$local];
            if ( $useTTL && $cachedVal[1] !== null && $frame ) {
                $frame->setTTL( $cachedVal[1] );
            }
            return $cachedVal[0];
        }
        # compute the timestamp string $ts
        # PHP >= 5.2 can handle dates before 1970 or after 2038 using the DateTime object
        $invalidTime = false;
        # the DateTime constructor must be used because it throws exceptions
        # when errors occur, whereas date_create appears to just output a warning
        # that can't really be detected from within the code
        try {
            # Default input timezone is UTC.
            $utc = new DateTimeZone( 'UTC' );
            # Correct for DateTime interpreting 'XXXX' as XX:XX o'clock
            if ( preg_match( '/^[0-9]{4}$/', $date ) ) {
                $date = '00:00 ' . $date;
            }
            # Parse date
            # UTC is a default input timezone.
            $dateObject = new DateTime( $date, $utc );
            # Set output timezone.
            if ( $local ) {
                if ( isset( $wgLocaltimezone ) ) {
                    $tz = new DateTimeZone( $wgLocaltimezone );
                } else {
                    $tz = new DateTimeZone( date_default_timezone_get() );
                }
            } else {
                $tz = $utc;
            }
            $dateObject->setTimezone( $tz );
            # Generate timestamp
            $ts = $dateObject->format( 'YmdHis' );
        } catch ( Exception $ex ) {
            $invalidTime = true;
        }
        $ttl = null;
        # format the timestamp and return the result
        if ( $invalidTime ) {
            $result = '<strong class="error">' .
                wfMessage( 'pfunc_time_error' )->inContentLanguage()->escaped() .
                '</strong>';
        } else {
            self::$mTimeChars += strlen( $format );
            if ( self::$mTimeChars > self::MAX_TIME_CHARS ) {
                return '<strong class="error">' .
                    wfMessage( 'pfunc_time_too_long' )->inContentLanguage()->escaped() .
                    '</strong>';
            } else {
                if ( $ts < 0 ) { // Language can't deal with BC years
                    return '<strong class="error">' .
                        wfMessage( 'pfunc_time_too_small' )->inContentLanguage()->escaped() .
                        '</strong>';
                } elseif ( $ts < 100000000000000 ) { // Language can't deal with years after 9999
                    if ( $language !== '' && Language::isValidBuiltInCode( $language ) ) {
                        // use whatever language is passed as a parameter
                        $langObject = Language::factory( $language );
                    } else {
                        // use wiki's content language
                        $langObject = $parser->getFunctionLang();
                        // $ttl is passed by reference, which doesn't work right on stub objects
                        StubObject::unstub( $langObject );
                    }
                    $result = $langObject->sprintfDate( $format, $ts, $tz, $ttl );
                } else {
                    return '<strong class="error">' .
                        wfMessage( 'pfunc_time_too_big' )->inContentLanguage()->escaped() .
                        '</strong>';
                }
            }
        }
        self::$mTimeCache[$format][$cacheKey][$language][$local] = [ $result, $ttl ];
        if ( $useTTL && $ttl !== null && $frame ) {
            $frame->setTTL( $ttl );
        }
        return $result;
    }
    /**
     * {{#time: format string }}
     * {{#time: format string | date/time object }}
     * {{#time: format string | date/time object | language code }}
     *
     * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time
     *
     * @param Parser $parser
     * @param PPFrame $frame
     * @param PPNode[] $args
     * @return string
     */
    public static function time( Parser $parser, PPFrame $frame, array $args ) {
        $format = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : '';
        $date = isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : '';
        $language = isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : '';
        $local = isset( $args[3] ) && trim( $frame->expand( $args[3] ) );
        return self::timeCommon( $parser, $frame, $format, $date, $language, $local );
    }
    /**
     * {{#timel: ... }}
     *
     * Identical to {{#time: ... }}, except that it uses the local time of the wiki
     * (as set in $wgLocaltimezone) when no date is given.
     *
     * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##timel
     *
     * @param Parser $parser
     * @param PPFrame $frame
     * @param PPNode[] $args
     * @return string
     */
    public static function localTime( Parser $parser, PPFrame $frame, array $args ) {
        $format = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : '';
        $date = isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : '';
        $language = isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : '';
        return self::timeCommon( $parser, $frame, $format, $date, $language, true );
    }
    /**
     * Obtain a specified number of slash-separated parts of a title,
     * e.g. {{#titleparts:Hello/World|1}} => "Hello"
     *
     * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##titleparts
     *
     * @param Parser $parser Parent parser
     * @param string $title Title to split
     * @param string|int $parts Number of parts to keep
     * @param string|int $offset Offset starting at 1
     * @return string
     */
    public static function titleparts( Parser $parser, $title = '', $parts = 0, $offset = 0 ) {
        $parts = (int)$parts;
        $offset = (int)$offset;
        $ntitle = Title::newFromText( $title );
        if ( $ntitle instanceof Title ) {
            $bits = explode( '/', $ntitle->getPrefixedText(), 25 );
            if ( count( $bits ) <= 0 ) {
                return $ntitle->getPrefixedText();
            } else {
                if ( $offset > 0 ) {
                    --$offset;
                }
                if ( $parts === 0 ) {
                    return implode( '/', array_slice( $bits, $offset ) );
                } else {
                    return implode( '/', array_slice( $bits, $offset, $parts ) );
                }
            }
        } else {
            return $title;
        }
    }
    /**
     * Verifies parameter is less than max string length.
     *
     * @param string $text
     * @return bool
     */
    private static function checkLength( $text ) {
        global $wgPFStringLengthLimit;
        return ( mb_strlen( $text ) < $wgPFStringLengthLimit );
    }
    /**
     * Generates error message. Called when string is too long.
     * @return string
     */
    private static function tooLongError() {
        global $wgPFStringLengthLimit;
        $msg = wfMessage( 'pfunc_string_too_long' )->numParams( $wgPFStringLengthLimit );
        return '<strong class="error">' . $msg->inContentLanguage()->escaped() . '</strong>';
    }
    /**
     * {{#len:string}}
     *
     * Reports number of characters in string.
     *
     * @param Parser $parser
     * @param string $inStr
     * @return int
     */
    public static function runLen( Parser $parser, $inStr = '' ) {
        $inStr = $parser->killMarkers( (string)$inStr );
        return mb_strlen( $inStr );
    }
    /**
     * {{#pos: string | needle | offset}}
     *
     * Finds first occurrence of "needle" in "string" starting at "offset".
     *
     * Note: If the needle is an empty string, single space is used instead.
     * Note: If the needle is not found, empty string is returned.
     * @param Parser $parser
     * @param string $inStr
     * @param string $inNeedle
     * @param string|int $inOffset
     * @return int|string
     */
    public static function runPos( Parser $parser, $inStr = '', $inNeedle = '', $inOffset = 0 ) {
        $inStr = $parser->killMarkers( (string)$inStr );
        $inNeedle = $parser->killMarkers( (string)$inNeedle );
        if ( !self::checkLength( $inStr ) ||
            !self::checkLength( $inNeedle ) ) {
            return self::tooLongError();
        }
        if ( $inNeedle === '' ) {
            $inNeedle = ' ';
        }
        $pos = mb_strpos( $inStr, $inNeedle, min( (int)$inOffset, mb_strlen( $inStr ) ) );
        if ( $pos === false ) {
            $pos = '';
        }
        return $pos;
    }
    /**
     * {{#rpos: string | needle}}
     *
     * Finds last occurrence of "needle" in "string".
     *
     * Note: If the needle is an empty string, single space is used instead.
     * Note: If the needle is not found, -1 is returned.
     * @param Parser $parser
     * @param string $inStr
     * @param string $inNeedle
     * @return int|string
     */
    public static function runRPos( Parser $parser, $inStr = '', $inNeedle = '' ) {
        $inStr = $parser->killMarkers( (string)$inStr );
        $inNeedle = $parser->killMarkers( (string)$inNeedle );
        if ( !self::checkLength( $inStr ) ||
            !self::checkLength( $inNeedle ) ) {
            return self::tooLongError();
        }
        if ( $inNeedle === '' ) {
            $inNeedle = ' ';
        }
        $pos = mb_strrpos( $inStr, $inNeedle );
        if ( $pos === false ) {
            $pos = -1;
        }
        return $pos;
    }
    /**
     * {{#sub: string | start | length }}
     *
     * Returns substring of "string" starting at "start" and having
     * "length" characters.
     *
     * Note: If length is zero, the rest of the input is returned.
     * Note: A negative value for "start" operates from the end of the
     *   "string".
     * Note: A negative value for "length" returns a string reduced in
     *   length by that amount.
     *
     * @param Parser $parser
     * @param string $inStr
     * @param string|int $inStart
     * @param string|int $inLength
     * @return string
     */
    public static function runSub( Parser $parser, $inStr = '', $inStart = 0, $inLength = 0 ) {
        $inStr = $parser->killMarkers( (string)$inStr );
        if ( !self::checkLength( $inStr ) ) {
            return self::tooLongError();
        }
        if ( (int)$inLength === 0 ) {
            $result = mb_substr( $inStr, (int)$inStart );
        } else {
            $result = mb_substr( $inStr, (int)$inStart, (int)$inLength );
        }
        return $result;
    }
    /**
     * {{#count: string | substr }}
     *
     * Returns number of occurrences of "substr" in "string".
     *
     * Note: If "substr" is empty, a single space is used.
     *
     * @param Parser $parser
     * @param string $inStr
     * @param string $inSubStr
     * @return int|string
     */
    public static function runCount( Parser $parser, $inStr = '', $inSubStr = '' ) {
        $inStr = $parser->killMarkers( (string)$inStr );
        $inSubStr = $parser->killMarkers( (string)$inSubStr );
        if ( !self::checkLength( $inStr ) ||
            !self::checkLength( $inSubStr ) ) {
            return self::tooLongError();
        }
        if ( $inSubStr === '' ) {
            $inSubStr = ' ';
        }
        $result = mb_substr_count( $inStr, $inSubStr );
        return $result;
    }
    /**
     * {{#replace:string | from | to | limit }}
     *
     * Replaces each occurrence of "from" in "string" with "to".
     * At most "limit" replacements are performed.
     *
     * Note: Armored against replacements that would generate huge strings.
     * Note: If "from" is an empty string, single space is used instead.
     *
     * @param Parser $parser
     * @param string $inStr
     * @param string $inReplaceFrom
     * @param string $inReplaceTo
     * @param string|int $inLimit
     * @return string
     */
    public static function runReplace( Parser $parser, $inStr = '',
            $inReplaceFrom = '', $inReplaceTo = '', $inLimit = -1 ) {
        global $wgPFStringLengthLimit;
        $inStr = $parser->killMarkers( (string)$inStr );
        $inReplaceFrom = $parser->killMarkers( (string)$inReplaceFrom );
        $inReplaceTo = $parser->killMarkers( (string)$inReplaceTo );
        if ( !self::checkLength( $inStr ) ||
            !self::checkLength( $inReplaceFrom ) ||
            !self::checkLength( $inReplaceTo ) ) {
            return self::tooLongError();
        }
        if ( $inReplaceFrom === '' ) {
            $inReplaceFrom = ' ';
        }
        // Precompute limit to avoid generating enormous string:
        $diff = mb_strlen( $inReplaceTo ) - mb_strlen( $inReplaceFrom );
        if ( $diff > 0 ) {
            $limit = ( ( $wgPFStringLengthLimit - mb_strlen( $inStr ) ) / $diff ) + 1;
        } else {
            $limit = -1;
        }
        $inLimit = (int)$inLimit;
        if ( $inLimit >= 0 ) {
            if ( $limit > $inLimit || $limit == -1 ) {
                $limit = $inLimit;
            }
        }
        // Use regex to allow limit and handle UTF-8 correctly.
        $inReplaceFrom = preg_quote( $inReplaceFrom, '/' );
        $inReplaceTo = StringUtils::escapeRegexReplacement( $inReplaceTo );
        $result = preg_replace( '/' . $inReplaceFrom . '/u',
                        $inReplaceTo, $inStr, $limit );
        if ( !self::checkLength( $result ) ) {
            return self::tooLongError();
        }
        return $result;
    }
    /**
     * {{#explode:string | delimiter | position | limit}}
     *
     * Breaks "string" into chunks separated by "delimiter" and returns the
     * chunk identified by "position".
     *
     * Note: Negative position can be used to specify tokens from the end.
     * Note: If the divider is an empty string, single space is used instead.
     * Note: Empty string is returned if there are not enough exploded chunks.
     *
     * @param Parser $parser
     * @param string $inStr
     * @param string $inDiv
     * @param string|int $inPos
     * @param string|null $inLim
     * @return string
     */
    public static function runExplode(
        Parser $parser, $inStr = '', $inDiv = '', $inPos = 0, $inLim = null
    ) {
        $inStr = $parser->killMarkers( (string)$inStr );
        $inDiv = $parser->killMarkers( (string)$inDiv );
        if ( $inDiv === '' ) {
            $inDiv = ' ';
        }
        if ( !self::checkLength( $inStr ) ||
            !self::checkLength( $inDiv ) ) {
            return self::tooLongError();
        }
        $inDiv = preg_quote( $inDiv, '/' );
        $matches = preg_split( '/' . $inDiv . '/u', $inStr, (int)$inLim );
        if ( $inPos >= 0 && isset( $matches[$inPos] ) ) {
            $result = $matches[$inPos];
        } elseif ( $inPos < 0 && isset( $matches[count( $matches ) + $inPos] ) ) {
            $result = $matches[count( $matches ) + $inPos];
        } else {
            $result = '';
        }
        return $result;
    }
    /**
     * {{#urldecode:string}}
     *
     * Decodes URL-encoded (like%20that) strings.
     *
     * @param Parser $parser
     * @param string $inStr
     * @return string
     */
    public static function runUrlDecode( Parser $parser, $inStr = '' ) {
        $inStr = $parser->killMarkers( (string)$inStr );
        if ( !self::checkLength( $inStr ) ) {
            return self::tooLongError();
        }
        return urldecode( $inStr );
    }
    /**
     * Take a PPNode (-ish thing), expand it, remove entities, and trim.
     *
     * For use when doing string comparisions, where user expects entities
     * to be equal for what they stand for (e.g. comparisions with {{PAGENAME}})
     *
     * @param PPNode|string $obj Thing to expand
     * @param PPFrame $frame
     * @param string|null &$trimExpanded Expanded and trimmed version of PPNode,
     *   but with char refs intact
     * @return string The trimmed, expanded and entity reference decoded version of the PPNode
     */
    private static function decodeTrimExpand( $obj, PPFrame $frame, &$trimExpanded = null ) {
        $expanded = $frame->expand( $obj );
        $trimExpanded = trim( $expanded );
        return trim( Sanitizer::decodeCharReferences( $expanded ) );
    }
    /**
     * @since 1.35
     * @param Language $language
     * @return ILanguageConverter
     */
    private static function getLanguageConverter( Language $language ): ILanguageConverter {
        return MediaWikiServices::getInstance()
            ->getLanguageConverterFactory()
            ->getLanguageConverter( $language );
    }
}