Make {{#language}} consistent with {{#dir}} and {{#bcp47}}
[mediawiki.git] / includes / parser / CoreMagicVariables.php
blobb3c28d5a28af74e203f981ea3bc27fbb3cb63fab
1 <?php
2 /**
3 * Magic variable implementations provided by MediaWiki core
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.
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.
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
20 * @file
21 * @ingroup Parser
23 use MediaWiki\Config\ServiceOptions;
24 use MediaWiki\MainConfigNames;
25 use MediaWiki\Parser\Parser;
26 use MediaWiki\Specials\SpecialVersion;
27 use MediaWiki\Utils\MWTimestamp;
28 use Psr\Log\LoggerInterface;
29 use Wikimedia\Timestamp\ConvertibleTimestamp;
31 /**
32 * Expansions of core magic variables, used by the parser.
33 * @internal
34 * @ingroup Parser
36 class 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,
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'
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;
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.
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();
95 $cacheTTL = self::CACHE_TTL_BY_ID[$id] ?? -1;
96 if ( $cacheTTL > -1 ) {
97 $parser->getOutput()->updateCacheExpiry( $cacheTTL );
100 switch ( $id ) {
101 case '!':
102 return '|';
103 case '=':
104 return '=';
105 case 'currentmonth':
106 self::applyUnitTimestampDeadline( $parser, $ts, 'M' );
108 return $pageLang->formatNumNoSeparators( $ts->format( 'm' ) );
109 case 'currentmonth1':
110 self::applyUnitTimestampDeadline( $parser, $ts, 'M' );
112 return $pageLang->formatNumNoSeparators( $ts->format( 'n' ) );
113 case 'currentmonthname':
114 self::applyUnitTimestampDeadline( $parser, $ts, 'M' );
116 return $pageLang->getMonthName( (int)$ts->format( 'n' ) );
117 case 'currentmonthnamegen':
118 self::applyUnitTimestampDeadline( $parser, $ts, 'M' );
120 return $pageLang->getMonthNameGen( (int)$ts->format( 'n' ) );
121 case 'currentmonthabbrev':
122 self::applyUnitTimestampDeadline( $parser, $ts, 'M' );
124 return $pageLang->getMonthAbbreviation( (int)$ts->format( 'n' ) );
125 case 'currentday':
126 self::applyUnitTimestampDeadline( $parser, $ts, 'D' );
128 return $pageLang->formatNumNoSeparators( $ts->format( 'j' ) );
129 case 'currentday2':
130 self::applyUnitTimestampDeadline( $parser, $ts, 'D' );
132 return $pageLang->formatNumNoSeparators( $ts->format( 'd' ) );
133 case 'localmonth':
134 $localTs = self::makeTsLocal( $svcOptions, $ts );
135 self::applyUnitTimestampDeadline( $parser, $localTs, 'M' );
137 return $pageLang->formatNumNoSeparators( $localTs->format( 'm' ) );
138 case 'localmonth1':
139 $localTs = self::makeTsLocal( $svcOptions, $ts );
140 self::applyUnitTimestampDeadline( $parser, $localTs, 'M' );
142 return $pageLang->formatNumNoSeparators( $localTs->format( 'n' ) );
143 case 'localmonthname':
144 $localTs = self::makeTsLocal( $svcOptions, $ts );
145 self::applyUnitTimestampDeadline( $parser, $localTs, 'M' );
147 return $pageLang->getMonthName( (int)$localTs->format( 'n' ) );
148 case 'localmonthnamegen':
149 $localTs = self::makeTsLocal( $svcOptions, $ts );
150 self::applyUnitTimestampDeadline( $parser, $localTs, 'M' );
152 return $pageLang->getMonthNameGen( (int)$localTs->format( 'n' ) );
153 case 'localmonthabbrev':
154 $localTs = self::makeTsLocal( $svcOptions, $ts );
155 self::applyUnitTimestampDeadline( $parser, $localTs, 'M' );
157 return $pageLang->getMonthAbbreviation( (int)$localTs->format( 'n' ) );
158 case 'localday':
159 $localTs = self::makeTsLocal( $svcOptions, $ts );
160 self::applyUnitTimestampDeadline( $parser, $localTs, 'D' );
162 return $pageLang->formatNumNoSeparators( $localTs->format( 'j' ) );
163 case 'localday2':
164 $localTs = self::makeTsLocal( $svcOptions, $ts );
165 self::applyUnitTimestampDeadline( $parser, $localTs, 'D' );
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".
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' );
214 return $pageLang->getWeekdayName( (int)$ts->format( 'w' ) + 1 );
215 case 'currentyear':
216 self::applyUnitTimestampDeadline( $parser, $ts, 'Y' );
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' );
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' );
233 return $pageLang->formatNum( $ts->format( 'w' ) );
234 case 'localdayname':
235 $localTs = self::makeTsLocal( $svcOptions, $ts );
236 self::applyUnitTimestampDeadline( $parser, $localTs, 'D' );
238 return $pageLang->getWeekdayName( (int)$localTs->format( 'w' ) + 1 );
239 case 'localyear':
240 $localTs = self::makeTsLocal( $svcOptions, $ts );
241 self::applyUnitTimestampDeadline( $parser, $localTs, 'Y' );
243 return $pageLang->formatNumNoSeparators( $localTs->format( 'Y' ) );
244 case 'localtime':
245 $localTs = self::makeTsLocal( $svcOptions, $ts );
247 return $pageLang->time(
248 $localTs->format( 'YmdHis' ),
249 false,
250 false
252 case 'localhour':
253 $localTs = self::makeTsLocal( $svcOptions, $ts );
254 self::applyUnitTimestampDeadline( $parser, $localTs, 'H' );
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' );
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 );
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 case 'bcp47':
304 case 'dir':
305 case 'language':
306 # magic variables are the same as empty/default first argument
307 return CoreParserFunctions::$id( $parser );
308 default:
309 // This is not one of the core magic variables
310 return null;
315 * Helper to convert a timestamp instance to local time
316 * @see MWTimestamp::getLocalInstance()
317 * @param ServiceOptions $svcOptions Service options for the parser
318 * @param ConvertibleTimestamp $ts Timestamp to convert
319 * @return ConvertibleTimestamp
321 private static function makeTsLocal( $svcOptions, $ts ) {
322 $localtimezone = $svcOptions->get( MainConfigNames::Localtimezone );
323 $ts->setTimezone( $localtimezone );
324 return $ts;
328 * Adjust the cache expiry to account for a dynamic timestamp displayed in output
330 * @param Parser $parser
331 * @param ConvertibleTimestamp $ts Current timestamp with the display timezone
332 * @param string $unit The unit the timestamp is expressed in; one of ("Y", "M", "D", "H")
334 private static function applyUnitTimestampDeadline(
335 Parser $parser,
336 ConvertibleTimestamp $ts,
337 string $unit
339 $tsUnix = (int)$ts->getTimestamp( TS_UNIX );
341 $date = new DateTime( "@$tsUnix" );
342 $date->setTimezone( $ts->getTimezone() );
343 $date->modify( self::DEADLINE_DATE_SPEC_BY_UNIT[$unit] );
344 if ( $unit === 'H' ) {
345 // Zero out the minutes/seconds
346 $date->setTime( intval( $date->format( 'H' ), 10 ), 0, 0 );
347 } else {
348 $date->setTime( 0, 0, 0 );
350 $deadlineUnix = (int)$date->format( 'U' );
352 $ttl = max( $deadlineUnix - $tsUnix, self::MIN_DEADLINE_TTL );
353 $ttl += self::DEADLINE_TTL_CLOCK_FUDGE;
354 $ttl += ( $deadlineUnix % self::DEADLINE_TTL_STAGGER_MAX );
356 $parser->getOutput()->updateCacheExpiry( $ttl );