wikimedia/mediawiki-core

View on GitHub
includes/parser/CoreMagicVariables.php

Summary

Maintainability
F
3 days
Test Coverage
<?php
/**
 * Magic variable implementations provided by MediaWiki core
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 * @ingroup Parser
 */
use MediaWiki\Config\ServiceOptions;
use MediaWiki\MainConfigNames;
use MediaWiki\Parser\Parser;
use MediaWiki\Specials\SpecialVersion;
use MediaWiki\Utils\MWTimestamp;
use Psr\Log\LoggerInterface;
use Wikimedia\Timestamp\ConvertibleTimestamp;

/**
 * Expansions of core magic variables, used by the parser.
 * @internal
 * @ingroup Parser
 */
class CoreMagicVariables {
    /** Map of (word ID => cache TTL hint) */
    private const CACHE_TTL_BY_ID = [
        'currenttime' => 3600,
        'localtime' => 3600,
        'numberofarticles' => 3600,
        'numberoffiles' => 3600,
        'numberofedits' => 3600,
        'numberofusers' => 3600,
        'numberofactiveusers' => 3600,
        'numberofpages' => 3600,
        'currentversion' => 86400,
        'currenttimestamp' => 3600,
        'localtimestamp' => 3600,
        'pagesinnamespace' => 3600,
        'numberofadmins' => 3600,
        'numberingroup' => 3600,
    ];

    /** Map of (time unit => relative datetime specifier) */
    private const DEADLINE_DATE_SPEC_BY_UNIT = [
        'Y' => 'first day of January next year midnight',
        'M' => 'first day of next month midnight',
        'D' => 'next day midnight',
        // Note that this relative datetime specifier does not zero out
        // minutes/seconds, but we will do so manually in
        // ::applyUnitTimestampDeadline() when given the unit 'H'
        'H' => 'next hour'
    ];
    /** Seconds of clock skew fudge factor for time-interval deadline TTLs */
    private const DEADLINE_TTL_CLOCK_FUDGE = 1;
    /** Max seconds to "randomly" add to time-interval deadline TTLs to avoid stampedes */
    private const DEADLINE_TTL_STAGGER_MAX = 15;
    /** Minimum time-interval deadline TTL */
    private const MIN_DEADLINE_TTL = 15;

    /**
     * Expand the magic variable given by $index.
     * @internal
     * @param Parser $parser
     * @param string $id The name of the variable, and equivalently, the magic
     *   word ID which was used to match the variable
     * @param ConvertibleTimestamp $ts Timestamp to use when expanding magic variable
     * @param ServiceOptions $svcOptions Service options for the parser
     * @param LoggerInterface $logger
     * @return string|null The expanded value, as wikitext, or null to
     *  indicate the given index wasn't a known magic variable.
     */
    public static function expand(
        // Fundamental options
        Parser $parser,
        string $id,
        // Context passed over from the parser
        ConvertibleTimestamp $ts,
        ServiceOptions $svcOptions,
        LoggerInterface $logger
    ): ?string {
        $pageLang = $parser->getTargetLanguage();

        $cacheTTL = self::CACHE_TTL_BY_ID[$id] ?? -1;
        if ( $cacheTTL > -1 ) {
            $parser->getOutput()->updateCacheExpiry( $cacheTTL );
        }

        switch ( $id ) {
            case '!':
                return '|';
            case '=':
                return '=';
            case 'currentmonth':
                self::applyUnitTimestampDeadline( $parser, $ts, 'M' );

                return $pageLang->formatNumNoSeparators( $ts->format( 'm' ) );
            case 'currentmonth1':
                self::applyUnitTimestampDeadline( $parser, $ts, 'M' );

                return $pageLang->formatNumNoSeparators( $ts->format( 'n' ) );
            case 'currentmonthname':
                self::applyUnitTimestampDeadline( $parser, $ts, 'M' );

                return $pageLang->getMonthName( (int)$ts->format( 'n' ) );
            case 'currentmonthnamegen':
                self::applyUnitTimestampDeadline( $parser, $ts, 'M' );

                return $pageLang->getMonthNameGen( (int)$ts->format( 'n' ) );
            case 'currentmonthabbrev':
                self::applyUnitTimestampDeadline( $parser, $ts, 'M' );

                return $pageLang->getMonthAbbreviation( (int)$ts->format( 'n' ) );
            case 'currentday':
                self::applyUnitTimestampDeadline( $parser, $ts, 'D' );

                return $pageLang->formatNumNoSeparators( $ts->format( 'j' ) );
            case 'currentday2':
                self::applyUnitTimestampDeadline( $parser, $ts, 'D' );

                return $pageLang->formatNumNoSeparators( $ts->format( 'd' ) );
            case 'localmonth':
                $localTs = self::makeTsLocal( $svcOptions, $ts );
                self::applyUnitTimestampDeadline( $parser, $localTs, 'M' );

                return $pageLang->formatNumNoSeparators( $localTs->format( 'm' ) );
            case 'localmonth1':
                $localTs = self::makeTsLocal( $svcOptions, $ts );
                self::applyUnitTimestampDeadline( $parser, $localTs, 'M' );

                return $pageLang->formatNumNoSeparators( $localTs->format( 'n' ) );
            case 'localmonthname':
                $localTs = self::makeTsLocal( $svcOptions, $ts );
                self::applyUnitTimestampDeadline( $parser, $localTs, 'M' );

                return $pageLang->getMonthName( (int)$localTs->format( 'n' ) );
            case 'localmonthnamegen':
                $localTs = self::makeTsLocal( $svcOptions, $ts );
                self::applyUnitTimestampDeadline( $parser, $localTs, 'M' );

                return $pageLang->getMonthNameGen( (int)$localTs->format( 'n' ) );
            case 'localmonthabbrev':
                $localTs = self::makeTsLocal( $svcOptions, $ts );
                self::applyUnitTimestampDeadline( $parser, $localTs, 'M' );

                return $pageLang->getMonthAbbreviation( (int)$localTs->format( 'n' ) );
            case 'localday':
                $localTs = self::makeTsLocal( $svcOptions, $ts );
                self::applyUnitTimestampDeadline( $parser, $localTs, 'D' );

                return $pageLang->formatNumNoSeparators( $localTs->format( 'j' ) );
            case 'localday2':
                $localTs = self::makeTsLocal( $svcOptions, $ts );
                self::applyUnitTimestampDeadline( $parser, $localTs, 'D' );

                return $pageLang->formatNumNoSeparators( $localTs->format( 'd' ) );
            case 'pagename':
            case 'pagenamee':
            case 'fullpagename':
            case 'fullpagenamee':
            case 'subpagename':
            case 'subpagenamee':
            case 'rootpagename':
            case 'rootpagenamee':
            case 'basepagename':
            case 'basepagenamee':
            case 'talkpagename':
            case 'talkpagenamee':
            case 'subjectpagename':
            case 'subjectpagenamee':
            case 'pageid':
            case 'revisionid':
            case 'revisionuser':
            case 'revisionday':
            case 'revisionday2':
            case 'revisionmonth':
            case 'revisionmonth1':
            case 'revisionyear':
            case 'revisiontimestamp':
            case 'namespace':
            case 'namespacee':
            case 'namespacenumber':
            case 'talkspace':
            case 'talkspacee':
            case 'subjectspace':
            case 'subjectspacee':
            case 'cascadingsources':
                # First argument of the corresponding parser function
                # (second argument of the PHP implementation) is
                # "title".

                # Note that for many of these {{FOO}} is subtly different
                # from {{FOO:{{PAGENAME}}}}, so we can't pass $title here
                # we have to explicitly use the "no arguments" form of the
                # parser function by passing `null` to indicate a missing
                # argument (which then defaults to the current page title).
                return CoreParserFunctions::$id( $parser, null );
            case 'revisionsize':
                return (string)$parser->getRevisionSize();
            case 'currentdayname':
                self::applyUnitTimestampDeadline( $parser, $ts, 'D' );

                return $pageLang->getWeekdayName( (int)$ts->format( 'w' ) + 1 );
            case 'currentyear':
                self::applyUnitTimestampDeadline( $parser, $ts, 'Y' );

                return $pageLang->formatNumNoSeparators( $ts->format( 'Y' ) );
            case 'currenttime':
                return $pageLang->time( $ts->getTimestamp( TS_MW ), false, false );
            case 'currenthour':
                self::applyUnitTimestampDeadline( $parser, $ts, 'H' );

                return $pageLang->formatNumNoSeparators( $ts->format( 'H' ) );
            case 'currentweek':
                self::applyUnitTimestampDeadline( $parser, $ts, 'D' );
                // @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
                // int to remove the padding
                return $pageLang->formatNum( (int)$ts->format( 'W' ) );
            case 'currentdow':
                self::applyUnitTimestampDeadline( $parser, $ts, 'D' );

                return $pageLang->formatNum( $ts->format( 'w' ) );
            case 'localdayname':
                $localTs = self::makeTsLocal( $svcOptions, $ts );
                self::applyUnitTimestampDeadline( $parser, $localTs, 'D' );

                return $pageLang->getWeekdayName( (int)$localTs->format( 'w' ) + 1 );
            case 'localyear':
                $localTs = self::makeTsLocal( $svcOptions, $ts );
                self::applyUnitTimestampDeadline( $parser, $localTs, 'Y' );

                return $pageLang->formatNumNoSeparators( $localTs->format( 'Y' ) );
            case 'localtime':
                $localTs = self::makeTsLocal( $svcOptions, $ts );

                return $pageLang->time(
                    $localTs->format( 'YmdHis' ),
                    false,
                    false
                );
            case 'localhour':
                $localTs = self::makeTsLocal( $svcOptions, $ts );
                self::applyUnitTimestampDeadline( $parser, $localTs, 'H' );

                return $pageLang->formatNumNoSeparators( $localTs->format( 'H' ) );
            case 'localweek':
                $localTs = self::makeTsLocal( $svcOptions, $ts );
                self::applyUnitTimestampDeadline( $parser, $localTs, 'D' );
                // @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
                // int to remove the padding
                return $pageLang->formatNum( (int)$localTs->format( 'W' ) );
            case 'localdow':
                $localTs = self::makeTsLocal( $svcOptions, $ts );
                self::applyUnitTimestampDeadline( $parser, $localTs, 'D' );

                return $pageLang->formatNum( $localTs->format( 'w' ) );
            case 'numberofarticles':
            case 'numberoffiles':
            case 'numberofusers':
            case 'numberofactiveusers':
            case 'numberofpages':
            case 'numberofadmins':
            case 'numberofedits':
                # second argument is 'raw'; magic variables are "not raw"
                return CoreParserFunctions::$id( $parser, null );
            case 'currenttimestamp':
                return $ts->getTimestamp( TS_MW );
            case 'localtimestamp':
                $localTs = self::makeTsLocal( $svcOptions, $ts );

                return $localTs->format( 'YmdHis' );
            case 'currentversion':
                return SpecialVersion::getVersion();
            case 'articlepath':
                return (string)$svcOptions->get( MainConfigNames::ArticlePath );
            case 'sitename':
                return (string)$svcOptions->get( MainConfigNames::Sitename );
            case 'server':
                return (string)$svcOptions->get( MainConfigNames::Server );
            case 'servername':
                return (string)$svcOptions->get( MainConfigNames::ServerName );
            case 'scriptpath':
                return (string)$svcOptions->get( MainConfigNames::ScriptPath );
            case 'stylepath':
                return (string)$svcOptions->get( MainConfigNames::StylePath );
            case 'directionmark':
                return $pageLang->getDirMark();
            case 'contentlanguage':
                return $parser->getContentLanguage()->getCode();
            case 'pagelanguage':
                return $pageLang->getCode();
            default:
                // This is not one of the core magic variables
                return null;
        }
    }

    /**
     * Helper to convert a timestamp instance to local time
     * @see MWTimestamp::getLocalInstance()
     * @param ServiceOptions $svcOptions Service options for the parser
     * @param ConvertibleTimestamp $ts Timestamp to convert
     * @return ConvertibleTimestamp
     */
    private static function makeTsLocal( $svcOptions, $ts ) {
        $localtimezone = $svcOptions->get( MainConfigNames::Localtimezone );
        $ts->setTimezone( $localtimezone );
        return $ts;
    }

    /**
     * Adjust the cache expiry to account for a dynamic timestamp displayed in output
     *
     * @param Parser $parser
     * @param ConvertibleTimestamp $ts Current timestamp with the display timezone
     * @param string $unit The unit the timestamp is expressed in; one of ("Y", "M", "D", "H")
     */
    private static function applyUnitTimestampDeadline(
        Parser $parser,
        ConvertibleTimestamp $ts,
        string $unit
    ) {
        $tsUnix = (int)$ts->getTimestamp( TS_UNIX );

        $date = new DateTime( "@$tsUnix" );
        $date->setTimezone( $ts->getTimezone() );
        $date->modify( self::DEADLINE_DATE_SPEC_BY_UNIT[$unit] );
        if ( $unit === 'H' ) {
            // Zero out the minutes/seconds
            $date->setTime( intval( $date->format( 'H' ), 10 ), 0, 0 );
        } else {
            $date->setTime( 0, 0, 0 );
        }
        $deadlineUnix = (int)$date->format( 'U' );

        $ttl = max( $deadlineUnix - $tsUnix, self::MIN_DEADLINE_TTL );
        $ttl += self::DEADLINE_TTL_CLOCK_FUDGE;
        $ttl += ( $deadlineUnix % self::DEADLINE_TTL_STAGGER_MAX );

        $parser->getOutput()->updateCacheExpiry( $ttl );
    }
}