Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.37% covered (success)
96.37%
186 / 193
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
CoreMagicVariables
96.88% covered (success)
96.88%
186 / 192
75.00% covered (warning)
75.00%
3 / 4
94
0.00% covered (danger)
0.00%
0 / 1
 expand
96.69% covered (success)
96.69%
175 / 181
0.00% covered (danger)
0.00%
0 / 1
90
 makeTsLocal
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 applyUnitTimestampDeadline
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 applyCacheExpiry
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Magic variable implementations provided by MediaWiki core
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup Parser
8 */
9
10namespace MediaWiki\Parser;
11
12use MediaWiki\Config\ServiceOptions;
13use MediaWiki\Language\Language;
14use MediaWiki\MainConfigNames;
15use MediaWiki\Specials\SpecialVersion;
16use MediaWiki\Utils\MWTimestamp;
17use Psr\Log\LoggerInterface;
18use Wikimedia\Timestamp\ConvertibleTimestamp;
19use Wikimedia\Timestamp\TimestampFormat as TS;
20
21/**
22 * Expansions of core magic variables, used by the parser.
23 * @internal
24 * @ingroup Parser
25 */
26class CoreMagicVariables {
27    /** Map of (word ID => cache TTL hint) */
28    private const CACHE_TTL_BY_ID = [
29        'currenttime' => 3600,
30        'localtime' => 3600,
31        'numberofarticles' => 3600,
32        'numberoffiles' => 3600,
33        'numberofedits' => 3600,
34        'numberofusers' => 3600,
35        'numberofactiveusers' => 3600,
36        'numberofpages' => 3600,
37        'currentversion' => 86400,
38        'currenttimestamp' => 3600,
39        'localtimestamp' => 3600,
40        'pagesinnamespace' => 3600,
41        'numberofadmins' => 3600,
42        'numberingroup' => 3600,
43    ];
44
45    /** Seconds of clock skew fudge factor for time-interval deadline TTLs */
46    private const DEADLINE_TTL_CLOCK_FUDGE = 1;
47
48    /** Max seconds to "randomly" add to time-interval deadline TTLs to avoid stampedes */
49    private const DEADLINE_TTL_STAGGER_MAX = 15;
50
51    /** Minimum time-interval deadline TTL */
52    private const MIN_DEADLINE_TTL = 30 * 60;
53
54    /**
55     * Expand the magic variable given by $index.
56     * @internal
57     * @param Parser $parser
58     * @param string $id The name of the variable, and equivalently, the magic
59     *   word ID which was used to match the variable
60     * @param ConvertibleTimestamp $ts Timestamp to use when expanding magic variable
61     * @param ServiceOptions $svcOptions Service options for the parser
62     * @param LoggerInterface $logger
63     * @param PPFrame|false $frame The current parser frame, used to attribute
64     *   cache-expiry changes to the page/template that triggered them.
65     * @return string|null The expanded value, as wikitext, or null to
66     *  indicate the given index wasn't a known magic variable.
67     */
68    public static function expand(
69        // Fundamental options
70        Parser $parser,
71        string $id,
72        // Context passed over from the parser
73        ConvertibleTimestamp $ts,
74        ServiceOptions $svcOptions,
75        LoggerInterface $logger,
76        $frame = false
77    ): ?string {
78        $pageLang = $parser->getTargetLanguage();
79
80        $title = $frame ? $frame->getTitle() : $parser->getTitle();
81        $source = $title->getPrefixedDBkey() . " ($id)";
82
83        $cacheTTL = self::CACHE_TTL_BY_ID[$id] ?? -1;
84        if ( $cacheTTL > -1 ) {
85            $parser->getOutput()->updateCacheExpiry( $cacheTTL, $source );
86        }
87
88        switch ( $id ) {
89            case '!':
90                return '|';
91            case '=':
92                return '=';
93            case 'currentmonth':
94                self::applyUnitTimestampDeadline( $parser, $ts, 'M', $source );
95
96                return $pageLang->formatNumNoSeparators( $ts->format( 'm' ) );
97            case 'currentmonth1':
98                self::applyUnitTimestampDeadline( $parser, $ts, 'M', $source );
99
100                return $pageLang->formatNumNoSeparators( $ts->format( 'n' ) );
101            case 'currentmonthname':
102                self::applyUnitTimestampDeadline( $parser, $ts, 'M', $source );
103
104                return $pageLang->getMonthName( (int)$ts->format( 'n' ) );
105            case 'currentmonthnamegen':
106                self::applyUnitTimestampDeadline( $parser, $ts, 'M', $source );
107
108                return $pageLang->getMonthNameGen( (int)$ts->format( 'n' ) );
109            case 'currentmonthabbrev':
110                self::applyUnitTimestampDeadline( $parser, $ts, 'M', $source );
111
112                return $pageLang->getMonthAbbreviation( (int)$ts->format( 'n' ) );
113            case 'currentday':
114                self::applyUnitTimestampDeadline( $parser, $ts, 'D', $source );
115
116                return $pageLang->formatNumNoSeparators( $ts->format( 'j' ) );
117            case 'currentday2':
118                self::applyUnitTimestampDeadline( $parser, $ts, 'D', $source );
119
120                return $pageLang->formatNumNoSeparators( $ts->format( 'd' ) );
121            case 'localmonth':
122                $localTs = self::makeTsLocal( $svcOptions, $ts );
123                self::applyUnitTimestampDeadline( $parser, $localTs, 'M', $source );
124
125                return $pageLang->formatNumNoSeparators( $localTs->format( 'm' ) );
126            case 'localmonth1':
127                $localTs = self::makeTsLocal( $svcOptions, $ts );
128                self::applyUnitTimestampDeadline( $parser, $localTs, 'M', $source );
129
130                return $pageLang->formatNumNoSeparators( $localTs->format( 'n' ) );
131            case 'localmonthname':
132                $localTs = self::makeTsLocal( $svcOptions, $ts );
133                self::applyUnitTimestampDeadline( $parser, $localTs, 'M', $source );
134
135                return $pageLang->getMonthName( (int)$localTs->format( 'n' ) );
136            case 'localmonthnamegen':
137                $localTs = self::makeTsLocal( $svcOptions, $ts );
138                self::applyUnitTimestampDeadline( $parser, $localTs, 'M', $source );
139
140                return $pageLang->getMonthNameGen( (int)$localTs->format( 'n' ) );
141            case 'localmonthabbrev':
142                $localTs = self::makeTsLocal( $svcOptions, $ts );
143                self::applyUnitTimestampDeadline( $parser, $localTs, 'M', $source );
144
145                return $pageLang->getMonthAbbreviation( (int)$localTs->format( 'n' ) );
146            case 'localday':
147                $localTs = self::makeTsLocal( $svcOptions, $ts );
148                self::applyUnitTimestampDeadline( $parser, $localTs, 'D', $source );
149
150                return $pageLang->formatNumNoSeparators( $localTs->format( 'j' ) );
151            case 'localday2':
152                $localTs = self::makeTsLocal( $svcOptions, $ts );
153                self::applyUnitTimestampDeadline( $parser, $localTs, 'D', $source );
154
155                return $pageLang->formatNumNoSeparators( $localTs->format( 'd' ) );
156            case 'pagename':
157            case 'pagenamee':
158            case 'fullpagename':
159            case 'fullpagenamee':
160            case 'subpagename':
161            case 'subpagenamee':
162            case 'rootpagename':
163            case 'rootpagenamee':
164            case 'basepagename':
165            case 'basepagenamee':
166            case 'talkpagename':
167            case 'talkpagenamee':
168            case 'subjectpagename':
169            case 'subjectpagenamee':
170            case 'pageid':
171            case 'revisionid':
172            case 'revisionuser':
173            case 'revisionday':
174            case 'revisionday2':
175            case 'revisionmonth':
176            case 'revisionmonth1':
177            case 'revisionyear':
178            case 'revisiontimestamp':
179            case 'namespace':
180            case 'namespacee':
181            case 'namespacenumber':
182            case 'talkspace':
183            case 'talkspacee':
184            case 'subjectspace':
185            case 'subjectspacee':
186            case 'cascadingsources':
187                # First argument of the corresponding parser function
188                # (second argument of the PHP implementation) is
189                # "title".
190
191                # Note that for many of these {{FOO}} is subtly different
192                # from {{FOO:{{PAGENAME}}}}, so we can't pass $title here
193                # we have to explicitly use the "no arguments" form of the
194                # parser function by passing `null` to indicate a missing
195                # argument (which then defaults to the current page title).
196                return CoreParserFunctions::$id( $parser, null );
197            case 'revisionsize':
198                return (string)$parser->getRevisionSize();
199            case 'currentdayname':
200                self::applyUnitTimestampDeadline( $parser, $ts, 'D', $source );
201
202                return $pageLang->getWeekdayName( (int)$ts->format( 'w' ) + 1 );
203            case 'currentyear':
204                self::applyUnitTimestampDeadline( $parser, $ts, 'Y', $source );
205
206                return $pageLang->formatNumNoSeparators( $ts->format( 'Y' ) );
207            case 'currenttime':
208                return $pageLang->time( $ts->getTimestamp( TS::MW ), false, false );
209            case 'currenthour':
210                self::applyUnitTimestampDeadline( $parser, $ts, 'H', $source );
211
212                return $pageLang->formatNumNoSeparators( $ts->format( 'H' ) );
213            case 'currentweek':
214                self::applyUnitTimestampDeadline( $parser, $ts, 'D', $source );
215                // @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
216                // int to remove the padding
217                return $pageLang->formatNum( (int)$ts->format( 'W' ) );
218            case 'currentdow':
219                self::applyUnitTimestampDeadline( $parser, $ts, 'D', $source );
220
221                return $pageLang->formatNum( $ts->format( 'w' ) );
222            case 'localdayname':
223                $localTs = self::makeTsLocal( $svcOptions, $ts );
224                self::applyUnitTimestampDeadline( $parser, $localTs, 'D', $source );
225
226                return $pageLang->getWeekdayName( (int)$localTs->format( 'w' ) + 1 );
227            case 'localyear':
228                $localTs = self::makeTsLocal( $svcOptions, $ts );
229                self::applyUnitTimestampDeadline( $parser, $localTs, 'Y', $source );
230
231                return $pageLang->formatNumNoSeparators( $localTs->format( 'Y' ) );
232            case 'localtime':
233                $localTs = self::makeTsLocal( $svcOptions, $ts );
234
235                return $pageLang->time(
236                    $localTs->format( 'YmdHis' ),
237                    false,
238                    false
239                );
240            case 'localhour':
241                $localTs = self::makeTsLocal( $svcOptions, $ts );
242                self::applyUnitTimestampDeadline( $parser, $localTs, 'H', $source );
243
244                return $pageLang->formatNumNoSeparators( $localTs->format( 'H' ) );
245            case 'localweek':
246                $localTs = self::makeTsLocal( $svcOptions, $ts );
247                self::applyUnitTimestampDeadline( $parser, $localTs, 'D', $source );
248                // @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
249                // int to remove the padding
250                return $pageLang->formatNum( (int)$localTs->format( 'W' ) );
251            case 'localdow':
252                $localTs = self::makeTsLocal( $svcOptions, $ts );
253                self::applyUnitTimestampDeadline( $parser, $localTs, 'D', $source );
254
255                return $pageLang->formatNum( $localTs->format( 'w' ) );
256            case 'numberofarticles':
257            case 'numberoffiles':
258            case 'numberofusers':
259            case 'numberofactiveusers':
260            case 'numberofpages':
261            case 'numberofadmins':
262            case 'numberofedits':
263                # second argument is 'raw'; magic variables are "not raw"
264                return CoreParserFunctions::$id( $parser, null );
265            case 'currenttimestamp':
266                return $ts->getTimestamp( TS::MW );
267            case 'localtimestamp':
268                $localTs = self::makeTsLocal( $svcOptions, $ts );
269
270                return $localTs->format( 'YmdHis' );
271            case 'currentversion':
272                return SpecialVersion::getVersion();
273            case 'articlepath':
274                return (string)$svcOptions->get( MainConfigNames::ArticlePath );
275            case 'sitename':
276                return (string)$svcOptions->get( MainConfigNames::Sitename );
277            case 'server':
278                return (string)$svcOptions->get( MainConfigNames::Server );
279            case 'servername':
280                return (string)$svcOptions->get( MainConfigNames::ServerName );
281            case 'scriptpath':
282                return (string)$svcOptions->get( MainConfigNames::ScriptPath );
283            case 'stylepath':
284                return (string)$svcOptions->get( MainConfigNames::StylePath );
285            case 'directionmark':
286                return $pageLang->getDirMark();
287            case 'contentlanguage':
288                return $parser->getContentLanguage()->getCode();
289            case 'pagelanguage':
290                return $pageLang->getCode();
291            case 'userlanguage':
292                if ( $parser->getOptions()->isMessage()
293                    || $svcOptions->get( MainConfigNames::ParserEnableUserLanguage )
294                ) {
295                    return $parser->getOptions()->getUserLang();
296                } else {
297                    return $pageLang->getCode();
298                }
299            case 'bcp47':
300            case 'contentmodel':
301            case 'dir':
302            case 'language':
303                # magic variables are the same as empty/default first argument
304                return CoreParserFunctions::$id( $parser );
305            default:
306                // This is not one of the core magic variables
307                return null;
308        }
309    }
310
311    /**
312     * Helper to convert a timestamp instance to local time
313     * @see MWTimestamp::getLocalInstance()
314     * @param ServiceOptions $svcOptions Service options for the parser
315     * @param ConvertibleTimestamp $ts Timestamp to convert
316     * @return ConvertibleTimestamp
317     */
318    private static function makeTsLocal( $svcOptions, $ts ) {
319        $localtimezone = $svcOptions->get( MainConfigNames::Localtimezone );
320        $ts->setTimezone( $localtimezone );
321        return $ts;
322    }
323
324    /**
325     * Adjust the cache expiry to account for a dynamic timestamp displayed in output
326     *
327     * @param Parser $parser
328     * @param ?ConvertibleTimestamp $ts Current timestamp with the display timezone.
329     *   If missing (null) the timestamp will be fetched from the ParserOptions.
330     * @param string $unit The unit the timestamp is expressed in; one of ("Y", "M", "D", "H")
331     * @param string|null $source Cache-expiry attribution label
332     */
333    public static function applyUnitTimestampDeadline(
334        Parser $parser,
335        ?ConvertibleTimestamp $ts,
336        string $unit,
337        ?string $source = null
338    ): void {
339        $date = $ts?->timestamp ?? $parser->getParseTime();
340        $ttl = Language::computeUnitTimestampDeadline( $date, $unit );
341        self::applyCacheExpiry( $parser, $ttl, $date->getTimestamp(), $source );
342    }
343
344    /**
345     * Apply the given $ttl to the cache expiry of the parser output,
346     * adjusting slightly to ensure a minimum TTL and avoid cache
347     * stampede.
348     * @param Parser $parser
349     * @param int $ttl The desired time-to-live, in seconds
350     * @param ?int $stagger An optional random value used to avoid stampede
351     * @param string|null $source Cache-expiry attribution label
352     */
353    public static function applyCacheExpiry(
354        Parser $parser,
355        int $ttl,
356        ?int $stagger = null,
357        ?string $source = null
358    ): void {
359        $stagger ??= abs( $ttl ) ?: time();
360        $ttl = max( $ttl, self::MIN_DEADLINE_TTL );
361        $ttl += self::DEADLINE_TTL_CLOCK_FUDGE;
362        $ttl += ( $stagger % self::DEADLINE_TTL_STAGGER_MAX );
363        $parser->getOutput()->updateCacheExpiry( $ttl, $source );
364    }
365}
366
367/** @deprecated class alias since 1.43 */
368class_alias( CoreMagicVariables::class, 'CoreMagicVariables' );