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