aboutsummaryrefslogtreecommitdiffstats
path: root/includes/parser/CoreMagicVariables.php
blob: b3c28d5a28af74e203f981ea3bc27fbb3cb63fab (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
<?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();
			case 'bcp47':
			case 'dir':
			case 'language':
				# magic variables are the same as empty/default first argument
				return CoreParserFunctions::$id( $parser );
			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 );
	}
}