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