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