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