Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 343 |
|
0.00% |
0 / 30 |
CRAP | |
0.00% |
0 / 1 |
ParserFunctions | |
0.00% |
0 / 343 |
|
0.00% |
0 / 30 |
20880 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
getExprParser | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
expr | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
ifexpr | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
if | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
30 | |||
ifeq | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
42 | |||
iferror | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
switch | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
156 | |||
rel2abs | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
132 | |||
ifexistInternal | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
110 | |||
ifexist | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
timeCommon | |
0.00% |
0 / 60 |
|
0.00% |
0 / 1 |
240 | |||
normalizeLangCode | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
time | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
30 | |||
localTime | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
20 | |||
timef | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
timefl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
timefCommon | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
42 | |||
titleparts | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
checkLength | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tooLongError | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
runLen | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
runPos | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
runRPos | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
runSub | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
runCount | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
runReplace | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
110 | |||
runExplode | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
72 | |||
runUrlDecode | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
decodeTrimExpand | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\ParserFunctions; |
4 | |
5 | use Config; |
6 | use DateTime; |
7 | use DateTimeZone; |
8 | use Exception; |
9 | use LinkCache; |
10 | use MediaWiki\HookContainer\HookContainer; |
11 | use MediaWiki\Languages\LanguageConverterFactory; |
12 | use MediaWiki\Languages\LanguageFactory; |
13 | use MediaWiki\Languages\LanguageNameUtils; |
14 | use MediaWiki\SpecialPage\SpecialPageFactory; |
15 | use MWTimestamp; |
16 | use Parser; |
17 | use PPFrame; |
18 | use PPNode; |
19 | use RepoGroup; |
20 | use Sanitizer; |
21 | use StringUtils; |
22 | use Title; |
23 | use Wikimedia\RequestTimeout\TimeoutException; |
24 | |
25 | /** |
26 | * Parser function handlers |
27 | * |
28 | * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions |
29 | */ |
30 | class ParserFunctions { |
31 | private static $mExprParser = null; |
32 | private static $mTimeCache = []; |
33 | private static $mTimeChars = 0; |
34 | |
35 | /** ~10 seconds */ |
36 | private const MAX_TIME_CHARS = 6000; |
37 | |
38 | /** @var Config */ |
39 | private $config; |
40 | |
41 | /** @var HookContainer */ |
42 | private $hookContainer; |
43 | |
44 | /** @var LanguageConverterFactory */ |
45 | private $languageConverterFactory; |
46 | |
47 | /** @var LanguageFactory */ |
48 | private $languageFactory; |
49 | |
50 | /** @var LanguageNameUtils */ |
51 | private $languageNameUtils; |
52 | |
53 | /** @var LinkCache */ |
54 | private $linkCache; |
55 | |
56 | /** @var RepoGroup */ |
57 | private $repoGroup; |
58 | |
59 | /** @var SpecialPageFactory */ |
60 | private $specialPageFactory; |
61 | |
62 | /** |
63 | * @param Config $config |
64 | * @param HookContainer $hookContainer |
65 | * @param LanguageConverterFactory $languageConverterFactory |
66 | * @param LanguageFactory $languageFactory |
67 | * @param LanguageNameUtils $languageNameUtils |
68 | * @param LinkCache $linkCache |
69 | * @param RepoGroup $repoGroup |
70 | * @param SpecialPageFactory $specialPageFactory |
71 | */ |
72 | public function __construct( |
73 | Config $config, |
74 | HookContainer $hookContainer, |
75 | LanguageConverterFactory $languageConverterFactory, |
76 | LanguageFactory $languageFactory, |
77 | LanguageNameUtils $languageNameUtils, |
78 | LinkCache $linkCache, |
79 | RepoGroup $repoGroup, |
80 | SpecialPageFactory $specialPageFactory |
81 | ) { |
82 | $this->config = $config; |
83 | $this->hookContainer = $hookContainer; |
84 | $this->languageConverterFactory = $languageConverterFactory; |
85 | $this->languageFactory = $languageFactory; |
86 | $this->languageNameUtils = $languageNameUtils; |
87 | $this->linkCache = $linkCache; |
88 | $this->repoGroup = $repoGroup; |
89 | $this->specialPageFactory = $specialPageFactory; |
90 | } |
91 | |
92 | /** |
93 | * @return ExprParser |
94 | */ |
95 | private static function &getExprParser() { |
96 | if ( self::$mExprParser === null ) { |
97 | self::$mExprParser = new ExprParser; |
98 | } |
99 | return self::$mExprParser; |
100 | } |
101 | |
102 | /** |
103 | * {{#expr: expression }} |
104 | * |
105 | * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##expr |
106 | * |
107 | * @param Parser $parser |
108 | * @param string $expr |
109 | * @return string |
110 | */ |
111 | public function expr( Parser $parser, $expr = '' ) { |
112 | try { |
113 | return self::getExprParser()->doExpression( $expr ); |
114 | } catch ( ExprError $e ) { |
115 | return '<strong class="error">' . htmlspecialchars( $e->getUserFriendlyMessage() ) . '</strong>'; |
116 | } |
117 | } |
118 | |
119 | /** |
120 | * {{#ifexpr: expression | value if true | value if false }} |
121 | * |
122 | * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##ifexpr |
123 | * |
124 | * @param Parser $parser |
125 | * @param PPFrame $frame |
126 | * @param PPNode[] $args |
127 | * @return string |
128 | */ |
129 | public function ifexpr( Parser $parser, PPFrame $frame, array $args ) { |
130 | $expr = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; |
131 | $then = $args[1] ?? ''; |
132 | $else = $args[2] ?? ''; |
133 | |
134 | try { |
135 | $result = self::getExprParser()->doExpression( $expr ); |
136 | if ( is_numeric( $result ) ) { |
137 | $result = (float)$result; |
138 | } |
139 | $result = $result ? $then : $else; |
140 | } catch ( ExprError $e ) { |
141 | return '<strong class="error">' . htmlspecialchars( $e->getUserFriendlyMessage() ) . '</strong>'; |
142 | } |
143 | |
144 | if ( is_object( $result ) ) { |
145 | $result = trim( $frame->expand( $result ) ); |
146 | } |
147 | |
148 | return $result; |
149 | } |
150 | |
151 | /** |
152 | * {{#if: test string | value if test string is not empty | value if test string is empty }} |
153 | * |
154 | * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##if |
155 | * |
156 | * @param Parser $parser |
157 | * @param PPFrame $frame |
158 | * @param PPNode[] $args |
159 | * @return string |
160 | */ |
161 | public function if( Parser $parser, PPFrame $frame, array $args ) { |
162 | $test = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; |
163 | if ( $test !== '' ) { |
164 | return isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : ''; |
165 | } else { |
166 | return isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : ''; |
167 | } |
168 | } |
169 | |
170 | /** |
171 | * {{#ifeq: string 1 | string 2 | value if identical | value if different }} |
172 | * |
173 | * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##ifeq |
174 | * |
175 | * @param Parser $parser |
176 | * @param PPFrame $frame |
177 | * @param PPNode[] $args |
178 | * @return string |
179 | */ |
180 | public function ifeq( Parser $parser, PPFrame $frame, array $args ) { |
181 | $left = isset( $args[0] ) ? self::decodeTrimExpand( $args[0], $frame ) : ''; |
182 | $right = isset( $args[1] ) ? self::decodeTrimExpand( $args[1], $frame ) : ''; |
183 | |
184 | // Strict compare is not possible here. 01 should equal 1 for example. |
185 | /** @noinspection TypeUnsafeComparisonInspection */ |
186 | if ( $left == $right ) { |
187 | return isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : ''; |
188 | } else { |
189 | return isset( $args[3] ) ? trim( $frame->expand( $args[3] ) ) : ''; |
190 | } |
191 | } |
192 | |
193 | /** |
194 | * {{#iferror: test string | value if error | value if no error }} |
195 | * |
196 | * Error is when the input string contains an HTML object with class="error", as |
197 | * generated by other parser functions such as #expr, #time and #rel2abs. |
198 | * |
199 | * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##iferror |
200 | * |
201 | * @param Parser $parser |
202 | * @param PPFrame $frame |
203 | * @param PPNode[] $args |
204 | * @return string |
205 | */ |
206 | public function iferror( Parser $parser, PPFrame $frame, array $args ) { |
207 | $test = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; |
208 | $then = $args[1] ?? false; |
209 | $else = $args[2] ?? false; |
210 | |
211 | if ( preg_match( |
212 | '/<(?:strong|span|p|div)\s(?:[^\s>]*\s+)*?class="(?:[^"\s>]*\s+)*?error(?:\s[^">]*)?"/', |
213 | $test ) |
214 | ) { |
215 | $result = $then; |
216 | } elseif ( $else === false ) { |
217 | $result = $test; |
218 | } else { |
219 | $result = $else; |
220 | } |
221 | if ( $result === false ) { |
222 | return ''; |
223 | } |
224 | |
225 | return trim( $frame->expand( $result ) ); |
226 | } |
227 | |
228 | /** |
229 | * {{#switch: comparison string |
230 | * | case = result |
231 | * | case = result |
232 | * | ... |
233 | * | default result |
234 | * }} |
235 | * |
236 | * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##switch |
237 | * |
238 | * @param Parser $parser |
239 | * @param PPFrame $frame |
240 | * @param PPNode[] $args |
241 | * @return string |
242 | */ |
243 | public function switch( Parser $parser, PPFrame $frame, array $args ) { |
244 | if ( count( $args ) === 0 ) { |
245 | return ''; |
246 | } |
247 | $primary = self::decodeTrimExpand( array_shift( $args ), $frame ); |
248 | $found = $defaultFound = false; |
249 | $default = null; |
250 | $lastItemHadNoEquals = false; |
251 | $lastItem = ''; |
252 | $mwDefault = $parser->getMagicWordFactory()->get( 'default' ); |
253 | foreach ( $args as $arg ) { |
254 | $bits = $arg->splitArg(); |
255 | $nameNode = $bits['name']; |
256 | $index = $bits['index']; |
257 | $valueNode = $bits['value']; |
258 | |
259 | if ( $index === '' ) { |
260 | # Found "=" |
261 | $lastItemHadNoEquals = false; |
262 | if ( $found ) { |
263 | # Multiple input match |
264 | return trim( $frame->expand( $valueNode ) ); |
265 | } |
266 | $test = self::decodeTrimExpand( $nameNode, $frame ); |
267 | /** @noinspection TypeUnsafeComparisonInspection */ |
268 | if ( $test == $primary ) { |
269 | # Found a match, return now |
270 | return trim( $frame->expand( $valueNode ) ); |
271 | } |
272 | if ( $defaultFound || $mwDefault->matchStartToEnd( $test ) ) { |
273 | $default = $valueNode; |
274 | $defaultFound = false; |
275 | } # else wrong case, continue |
276 | } else { |
277 | # Multiple input, single output |
278 | # If the value matches, set a flag and continue |
279 | $lastItemHadNoEquals = true; |
280 | // $lastItem is an "out" variable |
281 | $decodedTest = self::decodeTrimExpand( $valueNode, $frame, $lastItem ); |
282 | /** @noinspection TypeUnsafeComparisonInspection */ |
283 | if ( $decodedTest == $primary ) { |
284 | $found = true; |
285 | } elseif ( $mwDefault->matchStartToEnd( $decodedTest ) ) { |
286 | $defaultFound = true; |
287 | } |
288 | } |
289 | } |
290 | # Default case |
291 | # Check if the last item had no = sign, thus specifying the default case |
292 | if ( $lastItemHadNoEquals ) { |
293 | return $lastItem; |
294 | } |
295 | if ( $default === null ) { |
296 | return ''; |
297 | } |
298 | return trim( $frame->expand( $default ) ); |
299 | } |
300 | |
301 | /** |
302 | * {{#rel2abs: path }} or {{#rel2abs: path | base path }} |
303 | * |
304 | * Returns the absolute path to a subpage, relative to the current article |
305 | * title. Treats titles as slash-separated paths. |
306 | * |
307 | * Following subpage link syntax instead of standard path syntax, an |
308 | * initial slash is treated as a relative path, and vice versa. |
309 | * |
310 | * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##rel2abs |
311 | * |
312 | * @param Parser $parser |
313 | * @param string $to |
314 | * @param string $from |
315 | * |
316 | * @return string |
317 | */ |
318 | public function rel2abs( Parser $parser, $to = '', $from = '' ) { |
319 | $from = trim( $from ); |
320 | if ( $from === '' ) { |
321 | $from = $parser->getTitle()->getPrefixedText(); |
322 | } |
323 | |
324 | $to = rtrim( $to, ' /' ); |
325 | |
326 | // if we have an empty path, or just one containing a dot |
327 | if ( $to === '' || $to === '.' ) { |
328 | return $from; |
329 | } |
330 | |
331 | // if the path isn't relative |
332 | if ( substr( $to, 0, 1 ) !== '/' && |
333 | substr( $to, 0, 2 ) !== './' && |
334 | substr( $to, 0, 3 ) !== '../' && |
335 | $to !== '..' |
336 | ) { |
337 | $from = ''; |
338 | } |
339 | // Make a long path, containing both, enclose it in /.../ |
340 | $fullPath = '/' . $from . '/' . $to . '/'; |
341 | |
342 | // remove redundant current path dots |
343 | $fullPath = preg_replace( '!/(\./)+!', '/', $fullPath ); |
344 | |
345 | // remove double slashes |
346 | $fullPath = preg_replace( '!/{2,}!', '/', $fullPath ); |
347 | |
348 | // remove the enclosing slashes now |
349 | $fullPath = trim( $fullPath, '/' ); |
350 | $exploded = explode( '/', $fullPath ); |
351 | $newExploded = []; |
352 | |
353 | foreach ( $exploded as $current ) { |
354 | if ( $current === '..' ) { // removing one level |
355 | if ( !count( $newExploded ) ) { |
356 | // attempted to access a node above root node |
357 | $msg = wfMessage( 'pfunc_rel2abs_invalid_depth', $fullPath ) |
358 | ->inContentLanguage()->escaped(); |
359 | return '<strong class="error">' . $msg . '</strong>'; |
360 | } |
361 | // remove last level from the stack |
362 | array_pop( $newExploded ); |
363 | } else { |
364 | // add the current level to the stack |
365 | $newExploded[] = $current; |
366 | } |
367 | } |
368 | |
369 | // we can now join it again |
370 | return implode( '/', $newExploded ); |
371 | } |
372 | |
373 | /** |
374 | * @param Parser $parser |
375 | * @param string $titletext |
376 | * |
377 | * @return bool |
378 | */ |
379 | private function ifexistInternal( Parser $parser, $titletext ): bool { |
380 | $title = Title::newFromText( $titletext ); |
381 | $this->languageConverterFactory->getLanguageConverter( $parser->getContentLanguage() ) |
382 | ->findVariantLink( $titletext, $title, true ); |
383 | if ( !$title ) { |
384 | return false; |
385 | } |
386 | |
387 | if ( $title->getNamespace() === NS_MEDIA ) { |
388 | /* If namespace is specified as NS_MEDIA, then we want to |
389 | * check the physical file, not the "description" page. |
390 | */ |
391 | if ( !$parser->incrementExpensiveFunctionCount() ) { |
392 | return false; |
393 | } |
394 | $file = $this->repoGroup->findFile( $title ); |
395 | if ( !$file ) { |
396 | $parser->getOutput()->addImage( |
397 | $title->getDBKey(), false, false ); |
398 | return false; |
399 | } |
400 | $parser->getOutput()->addImage( |
401 | $file->getName(), $file->getTimestamp(), $file->getSha1() ); |
402 | return $file->exists(); |
403 | } |
404 | if ( $title->isSpecialPage() ) { |
405 | /* Don't bother with the count for special pages, |
406 | * since their existence can be checked without |
407 | * accessing the database. |
408 | */ |
409 | return $this->specialPageFactory->exists( $title->getDBkey() ); |
410 | } |
411 | if ( $title->isExternal() ) { |
412 | /* Can't check the existence of pages on other sites, |
413 | * so just return false. Makes a sort of sense, since |
414 | * they don't exist _locally_. |
415 | */ |
416 | return false; |
417 | } |
418 | $pdbk = $title->getPrefixedDBkey(); |
419 | $id = $this->linkCache->getGoodLinkID( $pdbk ); |
420 | if ( $id !== 0 ) { |
421 | $parser->getOutput()->addLink( $title, $id ); |
422 | return true; |
423 | } |
424 | if ( $this->linkCache->isBadLink( $pdbk ) ) { |
425 | $parser->getOutput()->addLink( $title, 0 ); |
426 | return false; |
427 | } |
428 | if ( !$parser->incrementExpensiveFunctionCount() ) { |
429 | return false; |
430 | } |
431 | $id = $title->getArticleID(); |
432 | $parser->getOutput()->addLink( $title, $id ); |
433 | |
434 | // bug 70495: don't just check whether the ID != 0 |
435 | return $title->exists(); |
436 | } |
437 | |
438 | /** |
439 | * {{#ifexist: page title | value if exists | value if doesn't exist }} |
440 | * |
441 | * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##ifexist |
442 | * |
443 | * @param Parser $parser |
444 | * @param PPFrame $frame |
445 | * @param PPNode[] $args |
446 | * @return string |
447 | */ |
448 | public function ifexist( Parser $parser, PPFrame $frame, array $args ) { |
449 | $title = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; |
450 | $then = $args[1] ?? null; |
451 | $else = $args[2] ?? null; |
452 | |
453 | $result = $this->ifexistInternal( $parser, $title ) ? $then : $else; |
454 | if ( $result === null ) { |
455 | return ''; |
456 | } |
457 | return trim( $frame->expand( $result ) ); |
458 | } |
459 | |
460 | /** |
461 | * Used by time() and localTime() |
462 | * |
463 | * @param Parser $parser |
464 | * @param PPFrame $frame |
465 | * @param string $format |
466 | * @param string $date |
467 | * @param string $language |
468 | * @param string|bool $local |
469 | * @return string |
470 | */ |
471 | private function timeCommon( |
472 | Parser $parser, PPFrame $frame, $format, $date, $language, $local |
473 | ) { |
474 | $this->hookContainer->register( |
475 | 'ParserClearState', |
476 | static function () { |
477 | self::$mTimeChars = 0; |
478 | } |
479 | ); |
480 | |
481 | if ( $date === '' ) { |
482 | $cacheKey = $parser->getOptions()->getTimestamp(); |
483 | $timestamp = new MWTimestamp( $cacheKey ); |
484 | $date = $timestamp->getTimestamp( TS_ISO_8601 ); |
485 | $useTTL = true; |
486 | } else { |
487 | $cacheKey = $date; |
488 | $useTTL = false; |
489 | } |
490 | if ( isset( self::$mTimeCache[$format][$cacheKey][$language][$local] ) ) { |
491 | $cachedVal = self::$mTimeCache[$format][$cacheKey][$language][$local]; |
492 | if ( $useTTL && $cachedVal[1] !== null ) { |
493 | $frame->setTTL( $cachedVal[1] ); |
494 | } |
495 | return $cachedVal[0]; |
496 | } |
497 | |
498 | # compute the timestamp string $ts |
499 | # PHP >= 5.2 can handle dates before 1970 or after 2038 using the DateTime object |
500 | |
501 | $invalidTime = false; |
502 | |
503 | # the DateTime constructor must be used because it throws exceptions |
504 | # when errors occur, whereas date_create appears to just output a warning |
505 | # that can't really be detected from within the code |
506 | try { |
507 | |
508 | # Default input timezone is UTC. |
509 | $utc = new DateTimeZone( 'UTC' ); |
510 | |
511 | # Correct for DateTime interpreting 'XXXX' as XX:XX o'clock |
512 | if ( preg_match( '/^[0-9]{4}$/', $date ) ) { |
513 | $date = '00:00 ' . $date; |
514 | } |
515 | |
516 | # Parse date |
517 | # UTC is a default input timezone. |
518 | $dateObject = new DateTime( $date, $utc ); |
519 | |
520 | # Set output timezone. |
521 | if ( $local ) { |
522 | $tz = new DateTimeZone( |
523 | $this->config->get( 'Localtimezone' ) ?? |
524 | date_default_timezone_get() |
525 | ); |
526 | } else { |
527 | $tz = $utc; |
528 | } |
529 | $dateObject->setTimezone( $tz ); |
530 | # Generate timestamp |
531 | $ts = $dateObject->format( 'YmdHis' ); |
532 | |
533 | } catch ( TimeoutException $ex ) { |
534 | // Unfortunately DateTime throws a generic Exception, but we can't |
535 | // ignore an exception generated by the RequestTimeout library. |
536 | throw $ex; |
537 | } catch ( Exception $ex ) { |
538 | $invalidTime = true; |
539 | } |
540 | |
541 | $ttl = null; |
542 | # format the timestamp and return the result |
543 | if ( $invalidTime ) { |
544 | $result = '<strong class="error">' . |
545 | wfMessage( 'pfunc_time_error' )->inContentLanguage()->escaped() . |
546 | '</strong>'; |
547 | } else { |
548 | self::$mTimeChars += strlen( $format ); |
549 | if ( self::$mTimeChars > self::MAX_TIME_CHARS ) { |
550 | return '<strong class="error">' . |
551 | wfMessage( 'pfunc_time_too_long' )->inContentLanguage()->escaped() . |
552 | '</strong>'; |
553 | } |
554 | |
555 | if ( $ts < 0 ) { // Language can't deal with BC years |
556 | return '<strong class="error">' . |
557 | wfMessage( 'pfunc_time_too_small' )->inContentLanguage()->escaped() . |
558 | '</strong>'; |
559 | } |
560 | if ( $ts >= 100000000000000 ) { // Language can't deal with years after 9999 |
561 | return '<strong class="error">' . |
562 | wfMessage( 'pfunc_time_too_big' )->inContentLanguage()->escaped() . |
563 | '</strong>'; |
564 | } |
565 | |
566 | $langObject = $this->languageFactory->getLanguage( |
567 | $this->normalizeLangCode( $parser, $language ) ); |
568 | $result = $langObject->sprintfDate( $format, $ts, $tz, $ttl ); |
569 | } |
570 | self::$mTimeCache[$format][$cacheKey][$language][$local] = [ $result, $ttl ]; |
571 | if ( $useTTL && $ttl !== null ) { |
572 | $frame->setTTL( $ttl ); |
573 | } |
574 | return $result; |
575 | } |
576 | |
577 | /** |
578 | * Convert an input string to a known language code for time formatting |
579 | * |
580 | * @param Parser $parser |
581 | * @param string $langCode |
582 | * @return string |
583 | */ |
584 | private function normalizeLangCode( Parser $parser, string $langCode ) { |
585 | if ( $langCode !== '' && $this->languageNameUtils->isKnownLanguageTag( $langCode ) ) { |
586 | return $langCode; |
587 | } else { |
588 | return $parser->getTargetLanguage()->getCode(); |
589 | } |
590 | } |
591 | |
592 | /** |
593 | * {{#time: format string }} |
594 | * {{#time: format string | date/time object }} |
595 | * {{#time: format string | date/time object | language code }} |
596 | * |
597 | * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time |
598 | * |
599 | * @param Parser $parser |
600 | * @param PPFrame $frame |
601 | * @param PPNode[] $args |
602 | * @return string |
603 | */ |
604 | public function time( Parser $parser, PPFrame $frame, array $args ) { |
605 | $format = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; |
606 | $date = isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : ''; |
607 | $language = isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : ''; |
608 | $local = isset( $args[3] ) && trim( $frame->expand( $args[3] ) ); |
609 | return $this->timeCommon( $parser, $frame, $format, $date, $language, $local ); |
610 | } |
611 | |
612 | /** |
613 | * {{#timel: ... }} |
614 | * |
615 | * Identical to {{#time: ... }}, except that it uses the local time of the wiki |
616 | * (as set in $wgLocaltimezone) when no date is given. |
617 | * |
618 | * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##timel |
619 | * |
620 | * @param Parser $parser |
621 | * @param PPFrame $frame |
622 | * @param PPNode[] $args |
623 | * @return string |
624 | */ |
625 | public function localTime( Parser $parser, PPFrame $frame, array $args ) { |
626 | $format = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; |
627 | $date = isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : ''; |
628 | $language = isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : ''; |
629 | return $this->timeCommon( $parser, $frame, $format, $date, $language, true ); |
630 | } |
631 | |
632 | /** |
633 | * Formatted time -- time with a symbolic rather than explicit format |
634 | * |
635 | * @param Parser $parser |
636 | * @param PPFrame $frame |
637 | * @param array $args |
638 | * @return string |
639 | */ |
640 | public function timef( Parser $parser, PPFrame $frame, array $args ) { |
641 | return $this->timefCommon( $parser, $frame, $args, false ); |
642 | } |
643 | |
644 | /** |
645 | * Formatted time -- time with a symbolic rather than explicit format |
646 | * Using the local timezone of the wiki. |
647 | * |
648 | * @param Parser $parser |
649 | * @param PPFrame $frame |
650 | * @param array $args |
651 | * @return string |
652 | */ |
653 | public function timefl( Parser $parser, PPFrame $frame, array $args ) { |
654 | return $this->timefCommon( $parser, $frame, $args, true ); |
655 | } |
656 | |
657 | /** |
658 | * Helper for timef and timefl |
659 | * |
660 | * @param Parser $parser |
661 | * @param PPFrame $frame |
662 | * @param array $args |
663 | * @param bool $local |
664 | * @return string |
665 | */ |
666 | private function timefCommon( Parser $parser, PPFrame $frame, array $args, $local ) { |
667 | $date = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; |
668 | $inputType = isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : ''; |
669 | |
670 | if ( $inputType !== '' ) { |
671 | $types = $parser->getMagicWordFactory()->newArray( [ |
672 | 'timef-time', |
673 | 'timef-date', |
674 | 'timef-both', |
675 | 'timef-pretty' |
676 | ] ); |
677 | $id = $types->matchStartToEnd( $inputType ); |
678 | if ( $id === false ) { |
679 | return '<strong class="error">' . |
680 | wfMessage( 'pfunc_timef_bad_format' ) . |
681 | '</strong>'; |
682 | } |
683 | $type = str_replace( 'timef-', '', $id ); |
684 | } else { |
685 | $type = 'both'; |
686 | } |
687 | |
688 | $langCode = isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : ''; |
689 | $langCode = $this->normalizeLangCode( $parser, $langCode ); |
690 | $lang = $this->languageFactory->getLanguage( $langCode ); |
691 | $format = $lang->getDateFormatString( $type, 'default' ); |
692 | return $this->timeCommon( $parser, $frame, $format, $date, $langCode, $local ); |
693 | } |
694 | |
695 | /** |
696 | * Obtain a specified number of slash-separated parts of a title, |
697 | * e.g. {{#titleparts:Hello/World|1}} => "Hello" |
698 | * |
699 | * @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##titleparts |
700 | * |
701 | * @param Parser $parser Parent parser |
702 | * @param string $title Title to split |
703 | * @param string|int $parts Number of parts to keep |
704 | * @param string|int $offset Offset starting at 1 |
705 | * @return string |
706 | */ |
707 | public function titleparts( Parser $parser, $title = '', $parts = 0, $offset = 0 ) { |
708 | $parts = (int)$parts; |
709 | $offset = (int)$offset; |
710 | $ntitle = Title::newFromText( $title ); |
711 | if ( !$ntitle ) { |
712 | return $title; |
713 | } |
714 | |
715 | $bits = explode( '/', $ntitle->getPrefixedText(), 25 ); |
716 | if ( $offset > 0 ) { |
717 | --$offset; |
718 | } |
719 | return implode( '/', array_slice( $bits, $offset, $parts ?: null ) ); |
720 | } |
721 | |
722 | /** |
723 | * Verifies parameter is less than max string length. |
724 | * |
725 | * @param string $text |
726 | * @return bool |
727 | */ |
728 | private function checkLength( $text ) { |
729 | return ( mb_strlen( $text ) < $this->config->get( 'PFStringLengthLimit' ) ); |
730 | } |
731 | |
732 | /** |
733 | * Generates error message. Called when string is too long. |
734 | * @return string |
735 | */ |
736 | private function tooLongError() { |
737 | $msg = wfMessage( 'pfunc_string_too_long' ) |
738 | ->numParams( $this->config->get( 'PFStringLengthLimit' ) ); |
739 | return '<strong class="error">' . $msg->inContentLanguage()->escaped() . '</strong>'; |
740 | } |
741 | |
742 | /** |
743 | * {{#len:string}} |
744 | * |
745 | * Reports number of characters in string. |
746 | * |
747 | * @param Parser $parser |
748 | * @param string $inStr |
749 | * @return int |
750 | */ |
751 | public function runLen( Parser $parser, $inStr = '' ) { |
752 | $inStr = $parser->killMarkers( (string)$inStr ); |
753 | return mb_strlen( $inStr ); |
754 | } |
755 | |
756 | /** |
757 | * {{#pos: string | needle | offset}} |
758 | * |
759 | * Finds first occurrence of "needle" in "string" starting at "offset". |
760 | * |
761 | * Note: If the needle is an empty string, single space is used instead. |
762 | * Note: If the needle is not found, empty string is returned. |
763 | * @param Parser $parser |
764 | * @param string $inStr |
765 | * @param string $inNeedle |
766 | * @param string|int $inOffset |
767 | * @return int|string |
768 | */ |
769 | public function runPos( Parser $parser, $inStr = '', $inNeedle = '', $inOffset = 0 ) { |
770 | $inStr = $parser->killMarkers( (string)$inStr ); |
771 | $inNeedle = $parser->killMarkers( (string)$inNeedle ); |
772 | |
773 | if ( !$this->checkLength( $inStr ) || |
774 | !$this->checkLength( $inNeedle ) ) { |
775 | return $this->tooLongError(); |
776 | } |
777 | |
778 | if ( $inNeedle === '' ) { |
779 | $inNeedle = ' '; |
780 | } |
781 | |
782 | $pos = mb_strpos( $inStr, $inNeedle, min( (int)$inOffset, mb_strlen( $inStr ) ) ); |
783 | if ( $pos === false ) { |
784 | $pos = ''; |
785 | } |
786 | |
787 | return $pos; |
788 | } |
789 | |
790 | /** |
791 | * {{#rpos: string | needle}} |
792 | * |
793 | * Finds last occurrence of "needle" in "string". |
794 | * |
795 | * Note: If the needle is an empty string, single space is used instead. |
796 | * Note: If the needle is not found, -1 is returned. |
797 | * @param Parser $parser |
798 | * @param string $inStr |
799 | * @param string $inNeedle |
800 | * @return int|string |
801 | */ |
802 | public function runRPos( Parser $parser, $inStr = '', $inNeedle = '' ) { |
803 | $inStr = $parser->killMarkers( (string)$inStr ); |
804 | $inNeedle = $parser->killMarkers( (string)$inNeedle ); |
805 | |
806 | if ( !$this->checkLength( $inStr ) || |
807 | !$this->checkLength( $inNeedle ) ) { |
808 | return $this->tooLongError(); |
809 | } |
810 | |
811 | if ( $inNeedle === '' ) { |
812 | $inNeedle = ' '; |
813 | } |
814 | |
815 | $pos = mb_strrpos( $inStr, $inNeedle ); |
816 | if ( $pos === false ) { |
817 | $pos = -1; |
818 | } |
819 | |
820 | return $pos; |
821 | } |
822 | |
823 | /** |
824 | * {{#sub: string | start | length }} |
825 | * |
826 | * Returns substring of "string" starting at "start" and having |
827 | * "length" characters. |
828 | * |
829 | * Note: If length is zero, the rest of the input is returned. |
830 | * Note: A negative value for "start" operates from the end of the |
831 | * "string". |
832 | * Note: A negative value for "length" returns a string reduced in |
833 | * length by that amount. |
834 | * |
835 | * @param Parser $parser |
836 | * @param string $inStr |
837 | * @param string|int $inStart |
838 | * @param string|int $inLength |
839 | * @return string |
840 | */ |
841 | public function runSub( Parser $parser, $inStr = '', $inStart = 0, $inLength = 0 ) { |
842 | $inStr = $parser->killMarkers( (string)$inStr ); |
843 | |
844 | if ( !$this->checkLength( $inStr ) ) { |
845 | return $this->tooLongError(); |
846 | } |
847 | |
848 | if ( (int)$inLength === 0 ) { |
849 | $result = mb_substr( $inStr, (int)$inStart ); |
850 | } else { |
851 | $result = mb_substr( $inStr, (int)$inStart, (int)$inLength ); |
852 | } |
853 | |
854 | return $result; |
855 | } |
856 | |
857 | /** |
858 | * {{#count: string | substr }} |
859 | * |
860 | * Returns number of occurrences of "substr" in "string". |
861 | * |
862 | * Note: If "substr" is empty, a single space is used. |
863 | * |
864 | * @param Parser $parser |
865 | * @param string $inStr |
866 | * @param string $inSubStr |
867 | * @return int|string |
868 | */ |
869 | public function runCount( Parser $parser, $inStr = '', $inSubStr = '' ) { |
870 | $inStr = $parser->killMarkers( (string)$inStr ); |
871 | $inSubStr = $parser->killMarkers( (string)$inSubStr ); |
872 | |
873 | if ( !$this->checkLength( $inStr ) || |
874 | !$this->checkLength( $inSubStr ) ) { |
875 | return $this->tooLongError(); |
876 | } |
877 | |
878 | if ( $inSubStr === '' ) { |
879 | $inSubStr = ' '; |
880 | } |
881 | |
882 | $result = mb_substr_count( $inStr, $inSubStr ); |
883 | |
884 | return $result; |
885 | } |
886 | |
887 | /** |
888 | * {{#replace:string | from | to | limit }} |
889 | * |
890 | * Replaces each occurrence of "from" in "string" with "to". |
891 | * At most "limit" replacements are performed. |
892 | * |
893 | * Note: Armored against replacements that would generate huge strings. |
894 | * Note: If "from" is an empty string, single space is used instead. |
895 | * |
896 | * @param Parser $parser |
897 | * @param string $inStr |
898 | * @param string $inReplaceFrom |
899 | * @param string $inReplaceTo |
900 | * @param string|int $inLimit |
901 | * @return string |
902 | */ |
903 | public function runReplace( Parser $parser, $inStr = '', |
904 | $inReplaceFrom = '', $inReplaceTo = '', $inLimit = -1 ) { |
905 | $inStr = $parser->killMarkers( (string)$inStr ); |
906 | $inReplaceFrom = $parser->killMarkers( (string)$inReplaceFrom ); |
907 | $inReplaceTo = $parser->killMarkers( (string)$inReplaceTo ); |
908 | |
909 | if ( !$this->checkLength( $inStr ) || |
910 | !$this->checkLength( $inReplaceFrom ) || |
911 | !$this->checkLength( $inReplaceTo ) ) { |
912 | return $this->tooLongError(); |
913 | } |
914 | |
915 | if ( $inReplaceFrom === '' ) { |
916 | $inReplaceFrom = ' '; |
917 | } |
918 | |
919 | // Precompute limit to avoid generating enormous string: |
920 | $diff = mb_strlen( $inReplaceTo ) - mb_strlen( $inReplaceFrom ); |
921 | if ( $diff > 0 ) { |
922 | $limit = (int)( ( $this->config->get( 'PFStringLengthLimit' ) - mb_strlen( $inStr ) ) / $diff ) + 1; |
923 | } else { |
924 | $limit = -1; |
925 | } |
926 | |
927 | $inLimit = (int)$inLimit; |
928 | if ( $inLimit >= 0 ) { |
929 | if ( $limit > $inLimit || $limit == -1 ) { |
930 | $limit = $inLimit; |
931 | } |
932 | } |
933 | |
934 | // Use regex to allow limit and handle UTF-8 correctly. |
935 | $inReplaceFrom = preg_quote( $inReplaceFrom, '/' ); |
936 | $inReplaceTo = StringUtils::escapeRegexReplacement( $inReplaceTo ); |
937 | |
938 | $result = preg_replace( '/' . $inReplaceFrom . '/u', |
939 | $inReplaceTo, $inStr, $limit ); |
940 | |
941 | if ( !$this->checkLength( $result ) ) { |
942 | return $this->tooLongError(); |
943 | } |
944 | |
945 | return $result; |
946 | } |
947 | |
948 | /** |
949 | * {{#explode:string | delimiter | position | limit}} |
950 | * |
951 | * Breaks "string" into chunks separated by "delimiter" and returns the |
952 | * chunk identified by "position". |
953 | * |
954 | * Note: Negative position can be used to specify tokens from the end. |
955 | * Note: If the divider is an empty string, single space is used instead. |
956 | * Note: Empty string is returned if there are not enough exploded chunks. |
957 | * |
958 | * @param Parser $parser |
959 | * @param string $inStr |
960 | * @param string $inDiv |
961 | * @param string|int $inPos |
962 | * @param string|null $inLim |
963 | * @return string |
964 | */ |
965 | public function runExplode( |
966 | Parser $parser, $inStr = '', $inDiv = '', $inPos = 0, $inLim = null |
967 | ) { |
968 | $inStr = $parser->killMarkers( (string)$inStr ); |
969 | $inDiv = $parser->killMarkers( (string)$inDiv ); |
970 | |
971 | if ( $inDiv === '' ) { |
972 | $inDiv = ' '; |
973 | } |
974 | |
975 | if ( !$this->checkLength( $inStr ) || |
976 | !$this->checkLength( $inDiv ) ) { |
977 | return $this->tooLongError(); |
978 | } |
979 | |
980 | $inDiv = preg_quote( $inDiv, '/' ); |
981 | |
982 | $matches = preg_split( '/' . $inDiv . '/u', $inStr, (int)$inLim ); |
983 | |
984 | if ( $inPos >= 0 && isset( $matches[$inPos] ) ) { |
985 | $result = $matches[$inPos]; |
986 | } elseif ( $inPos < 0 && isset( $matches[count( $matches ) + $inPos] ) ) { |
987 | $result = $matches[count( $matches ) + $inPos]; |
988 | } else { |
989 | $result = ''; |
990 | } |
991 | |
992 | return $result; |
993 | } |
994 | |
995 | /** |
996 | * {{#urldecode:string}} |
997 | * |
998 | * Decodes URL-encoded (like%20that) strings. |
999 | * |
1000 | * @param Parser $parser |
1001 | * @param string $inStr |
1002 | * @return string |
1003 | */ |
1004 | public function runUrlDecode( Parser $parser, $inStr = '' ) { |
1005 | $inStr = $parser->killMarkers( (string)$inStr ); |
1006 | if ( !$this->checkLength( $inStr ) ) { |
1007 | return $this->tooLongError(); |
1008 | } |
1009 | |
1010 | return urldecode( $inStr ); |
1011 | } |
1012 | |
1013 | /** |
1014 | * Take a PPNode (-ish thing), expand it, remove entities, and trim. |
1015 | * |
1016 | * For use when doing string comparisions, where user expects entities |
1017 | * to be equal for what they stand for (e.g. comparisions with {{PAGENAME}}) |
1018 | * |
1019 | * @param PPNode|string $obj Thing to expand |
1020 | * @param PPFrame $frame |
1021 | * @param string &$trimExpanded @phan-output-reference Expanded and trimmed version of PPNode, |
1022 | * but with char refs intact |
1023 | * @return string The trimmed, expanded and entity reference decoded version of the PPNode |
1024 | */ |
1025 | private static function decodeTrimExpand( $obj, PPFrame $frame, &$trimExpanded = '' ) { |
1026 | $expanded = $frame->expand( $obj ); |
1027 | $trimExpanded = trim( $expanded ); |
1028 | return trim( Sanitizer::decodeCharReferences( $expanded ) ); |
1029 | } |
1030 | } |