Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
96.37% |
186 / 193 |
|
75.00% |
3 / 4 |
CRAP | |
0.00% |
0 / 1 |
| CoreMagicVariables | |
96.88% |
186 / 192 |
|
75.00% |
3 / 4 |
94 | |
0.00% |
0 / 1 |
| expand | |
96.69% |
175 / 181 |
|
0.00% |
0 / 1 |
90 | |||
| makeTsLocal | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| applyUnitTimestampDeadline | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| applyCacheExpiry | |
100.00% |
5 / 5 |
|
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 | |
| 10 | namespace MediaWiki\Parser; |
| 11 | |
| 12 | use MediaWiki\Config\ServiceOptions; |
| 13 | use MediaWiki\Language\Language; |
| 14 | use MediaWiki\MainConfigNames; |
| 15 | use MediaWiki\Specials\SpecialVersion; |
| 16 | use MediaWiki\Utils\MWTimestamp; |
| 17 | use Psr\Log\LoggerInterface; |
| 18 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
| 19 | use Wikimedia\Timestamp\TimestampFormat as TS; |
| 20 | |
| 21 | /** |
| 22 | * Expansions of core magic variables, used by the parser. |
| 23 | * @internal |
| 24 | * @ingroup Parser |
| 25 | */ |
| 26 | class 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 */ |
| 368 | class_alias( CoreMagicVariables::class, 'CoreMagicVariables' ); |