MediaWiki REL1_32
Preprocessor_Hash.php
Go to the documentation of this file.
1<?php
42// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
44
48 public $parser;
49
50 const CACHE_PREFIX = 'preprocess-hash';
51 const CACHE_VERSION = 2;
52
53 public function __construct( $parser ) {
54 $this->parser = $parser;
55 }
56
60 public function newFrame() {
61 return new PPFrame_Hash( $this );
62 }
63
68 public function newCustomFrame( $args ) {
69 return new PPCustomFrame_Hash( $this, $args );
70 }
71
76 public function newPartNodeArray( $values ) {
77 $list = [];
78
79 foreach ( $values as $k => $val ) {
80 if ( is_int( $k ) ) {
81 $store = [ [ 'part', [
82 [ 'name', [ [ '@index', [ $k ] ] ] ],
83 [ 'value', [ strval( $val ) ] ],
84 ] ] ];
85 } else {
86 $store = [ [ 'part', [
87 [ 'name', [ strval( $k ) ] ],
88 '=',
89 [ 'value', [ strval( $val ) ] ],
90 ] ] ];
91 }
92
93 $list[] = new PPNode_Hash_Tree( $store, 0 );
94 }
95
96 $node = new PPNode_Hash_Array( $list );
97 return $node;
98 }
99
118 public function preprocessToObj( $text, $flags = 0 ) {
120
121 $tree = $this->cacheGetTree( $text, $flags );
122 if ( $tree !== false ) {
123 $store = json_decode( $tree );
124 if ( is_array( $store ) ) {
125 return new PPNode_Hash_Tree( $store, 0 );
126 }
127 }
128
129 $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
130
131 $xmlishElements = $this->parser->getStripList();
132 $xmlishAllowMissingEndTag = [ 'includeonly', 'noinclude', 'onlyinclude' ];
133 $enableOnlyinclude = false;
134 if ( $forInclusion ) {
135 $ignoredTags = [ 'includeonly', '/includeonly' ];
136 $ignoredElements = [ 'noinclude' ];
137 $xmlishElements[] = 'noinclude';
138 if ( strpos( $text, '<onlyinclude>' ) !== false
139 && strpos( $text, '</onlyinclude>' ) !== false
140 ) {
141 $enableOnlyinclude = true;
142 }
143 } else {
144 $ignoredTags = [ 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ];
145 $ignoredElements = [ 'includeonly' ];
146 $xmlishElements[] = 'includeonly';
147 }
148 $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
149
150 // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
151 $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
152
153 $stack = new PPDStack_Hash;
154
155 $searchBase = "[{<\n";
157 $searchBase .= '-';
158 }
159
160 // For fast reverse searches
161 $revText = strrev( $text );
162 $lengthText = strlen( $text );
163
164 // Input pointer, starts out pointing to a pseudo-newline before the start
165 $i = 0;
166 // Current accumulator. See the doc comment for Preprocessor_Hash for the format.
167 $accum =& $stack->getAccum();
168 // True to find equals signs in arguments
169 $findEquals = false;
170 // True to take notice of pipe characters
171 $findPipe = false;
172 $headingIndex = 1;
173 // True if $i is inside a possible heading
174 $inHeading = false;
175 // True if there are no more greater-than (>) signs right of $i
176 $noMoreGT = false;
177 // Map of tag name => true if there are no more closing tags of given type right of $i
178 $noMoreClosingTag = [];
179 // True to ignore all input up to the next <onlyinclude>
180 $findOnlyinclude = $enableOnlyinclude;
181 // Do a line-start run without outputting an LF character
182 $fakeLineStart = true;
183
184 while ( true ) {
185 // $this->memCheck();
186
187 if ( $findOnlyinclude ) {
188 // Ignore all input up to the next <onlyinclude>
189 $startPos = strpos( $text, '<onlyinclude>', $i );
190 if ( $startPos === false ) {
191 // Ignored section runs to the end
192 $accum[] = [ 'ignore', [ substr( $text, $i ) ] ];
193 break;
194 }
195 $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
196 $accum[] = [ 'ignore', [ substr( $text, $i, $tagEndPos - $i ) ] ];
197 $i = $tagEndPos;
198 $findOnlyinclude = false;
199 }
200
201 if ( $fakeLineStart ) {
202 $found = 'line-start';
203 $curChar = '';
204 } else {
205 # Find next opening brace, closing brace or pipe
206 $search = $searchBase;
207 if ( $stack->top === false ) {
208 $currentClosing = '';
209 } else {
210 $currentClosing = $stack->top->close;
211 $search .= $currentClosing;
212 }
213 if ( $findPipe ) {
214 $search .= '|';
215 }
216 if ( $findEquals ) {
217 // First equals will be for the template
218 $search .= '=';
219 }
220 $rule = null;
221 # Output literal section, advance input counter
222 $literalLength = strcspn( $text, $search, $i );
223 if ( $literalLength > 0 ) {
224 self::addLiteral( $accum, substr( $text, $i, $literalLength ) );
225 $i += $literalLength;
226 }
227 if ( $i >= $lengthText ) {
228 if ( $currentClosing == "\n" ) {
229 // Do a past-the-end run to finish off the heading
230 $curChar = '';
231 $found = 'line-end';
232 } else {
233 # All done
234 break;
235 }
236 } else {
237 $curChar = $curTwoChar = $text[$i];
238 if ( ( $i + 1 ) < $lengthText ) {
239 $curTwoChar .= $text[$i + 1];
240 }
241 if ( $curChar == '|' ) {
242 $found = 'pipe';
243 } elseif ( $curChar == '=' ) {
244 $found = 'equals';
245 } elseif ( $curChar == '<' ) {
246 $found = 'angle';
247 } elseif ( $curChar == "\n" ) {
248 if ( $inHeading ) {
249 $found = 'line-end';
250 } else {
251 $found = 'line-start';
252 }
253 } elseif ( $curTwoChar == $currentClosing ) {
254 $found = 'close';
255 $curChar = $curTwoChar;
256 } elseif ( $curChar == $currentClosing ) {
257 $found = 'close';
258 } elseif ( isset( $this->rules[$curTwoChar] ) ) {
259 $curChar = $curTwoChar;
260 $found = 'open';
261 $rule = $this->rules[$curChar];
262 } elseif ( isset( $this->rules[$curChar] ) ) {
263 $found = 'open';
264 $rule = $this->rules[$curChar];
265 } else {
266 # Some versions of PHP have a strcspn which stops on
267 # null characters; ignore these and continue.
268 # We also may get '-' and '}' characters here which
269 # don't match -{ or $currentClosing. Add these to
270 # output and continue.
271 if ( $curChar == '-' || $curChar == '}' ) {
272 self::addLiteral( $accum, $curChar );
273 }
274 ++$i;
275 continue;
276 }
277 }
278 }
279
280 if ( $found == 'angle' ) {
281 $matches = false;
282 // Handle </onlyinclude>
283 if ( $enableOnlyinclude
284 && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>'
285 ) {
286 $findOnlyinclude = true;
287 continue;
288 }
289
290 // Determine element name
291 if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
292 // Element name missing or not listed
293 self::addLiteral( $accum, '<' );
294 ++$i;
295 continue;
296 }
297 // Handle comments
298 if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
299 // To avoid leaving blank lines, when a sequence of
300 // space-separated comments is both preceded and followed by
301 // a newline (ignoring spaces), then
302 // trim leading and trailing spaces and the trailing newline.
303
304 // Find the end
305 $endPos = strpos( $text, '-->', $i + 4 );
306 if ( $endPos === false ) {
307 // Unclosed comment in input, runs to end
308 $inner = substr( $text, $i );
309 $accum[] = [ 'comment', [ $inner ] ];
310 $i = $lengthText;
311 } else {
312 // Search backwards for leading whitespace
313 $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0;
314
315 // Search forwards for trailing whitespace
316 // $wsEnd will be the position of the last space (or the '>' if there's none)
317 $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 );
318
319 // Keep looking forward as long as we're finding more
320 // comments.
321 $comments = [ [ $wsStart, $wsEnd ] ];
322 while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) {
323 $c = strpos( $text, '-->', $wsEnd + 4 );
324 if ( $c === false ) {
325 break;
326 }
327 $c = $c + 2 + strspn( $text, " \t", $c + 3 );
328 $comments[] = [ $wsEnd + 1, $c ];
329 $wsEnd = $c;
330 }
331
332 // Eat the line if possible
333 // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
334 // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
335 // it's a possible beneficial b/c break.
336 if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
337 && substr( $text, $wsEnd + 1, 1 ) == "\n"
338 ) {
339 // Remove leading whitespace from the end of the accumulator
340 $wsLength = $i - $wsStart;
341 $endIndex = count( $accum ) - 1;
342
343 // Sanity check
344 if ( $wsLength > 0
345 && $endIndex >= 0
346 && is_string( $accum[$endIndex] )
347 && strspn( $accum[$endIndex], " \t", -$wsLength ) === $wsLength
348 ) {
349 $accum[$endIndex] = substr( $accum[$endIndex], 0, -$wsLength );
350 }
351
352 // Dump all but the last comment to the accumulator
353 foreach ( $comments as $j => $com ) {
354 $startPos = $com[0];
355 $endPos = $com[1] + 1;
356 if ( $j == ( count( $comments ) - 1 ) ) {
357 break;
358 }
359 $inner = substr( $text, $startPos, $endPos - $startPos );
360 $accum[] = [ 'comment', [ $inner ] ];
361 }
362
363 // Do a line-start run next time to look for headings after the comment
364 $fakeLineStart = true;
365 } else {
366 // No line to eat, just take the comment itself
367 $startPos = $i;
368 $endPos += 2;
369 }
370
371 if ( $stack->top ) {
372 $part = $stack->top->getCurrentPart();
373 if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) {
374 $part->visualEnd = $wsStart;
375 }
376 // Else comments abutting, no change in visual end
377 $part->commentEnd = $endPos;
378 }
379 $i = $endPos + 1;
380 $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
381 $accum[] = [ 'comment', [ $inner ] ];
382 }
383 continue;
384 }
385 $name = $matches[1];
386 $lowerName = strtolower( $name );
387 $attrStart = $i + strlen( $name ) + 1;
388
389 // Find end of tag
390 $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
391 if ( $tagEndPos === false ) {
392 // Infinite backtrack
393 // Disable tag search to prevent worst-case O(N^2) performance
394 $noMoreGT = true;
395 self::addLiteral( $accum, '<' );
396 ++$i;
397 continue;
398 }
399
400 // Handle ignored tags
401 if ( in_array( $lowerName, $ignoredTags ) ) {
402 $accum[] = [ 'ignore', [ substr( $text, $i, $tagEndPos - $i + 1 ) ] ];
403 $i = $tagEndPos + 1;
404 continue;
405 }
406
407 $tagStartPos = $i;
408 if ( $text[$tagEndPos - 1] == '/' ) {
409 // Short end tag
410 $attrEnd = $tagEndPos - 1;
411 $inner = null;
412 $i = $tagEndPos + 1;
413 $close = null;
414 } else {
415 $attrEnd = $tagEndPos;
416 // Find closing tag
417 if (
418 !isset( $noMoreClosingTag[$name] ) &&
419 preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
420 $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 )
421 ) {
422 $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
423 $i = $matches[0][1] + strlen( $matches[0][0] );
424 $close = $matches[0][0];
425 } else {
426 // No end tag
427 if ( in_array( $name, $xmlishAllowMissingEndTag ) ) {
428 // Let it run out to the end of the text.
429 $inner = substr( $text, $tagEndPos + 1 );
430 $i = $lengthText;
431 $close = null;
432 } else {
433 // Don't match the tag, treat opening tag as literal and resume parsing.
434 $i = $tagEndPos + 1;
435 self::addLiteral( $accum,
436 substr( $text, $tagStartPos, $tagEndPos + 1 - $tagStartPos ) );
437 // Cache results, otherwise we have O(N^2) performance for input like <foo><foo><foo>...
438 $noMoreClosingTag[$name] = true;
439 continue;
440 }
441 }
442 }
443 // <includeonly> and <noinclude> just become <ignore> tags
444 if ( in_array( $lowerName, $ignoredElements ) ) {
445 $accum[] = [ 'ignore', [ substr( $text, $tagStartPos, $i - $tagStartPos ) ] ];
446 continue;
447 }
448
449 if ( $attrEnd <= $attrStart ) {
450 $attr = '';
451 } else {
452 // Note that the attr element contains the whitespace between name and attribute,
453 // this is necessary for precise reconstruction during pre-save transform.
454 $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
455 }
456
457 $children = [
458 [ 'name', [ $name ] ],
459 [ 'attr', [ $attr ] ] ];
460 if ( $inner !== null ) {
461 $children[] = [ 'inner', [ $inner ] ];
462 }
463 if ( $close !== null ) {
464 $children[] = [ 'close', [ $close ] ];
465 }
466 $accum[] = [ 'ext', $children ];
467 } elseif ( $found == 'line-start' ) {
468 // Is this the start of a heading?
469 // Line break belongs before the heading element in any case
470 if ( $fakeLineStart ) {
471 $fakeLineStart = false;
472 } else {
473 self::addLiteral( $accum, $curChar );
474 $i++;
475 }
476
477 $count = strspn( $text, '=', $i, 6 );
478 if ( $count == 1 && $findEquals ) {
479 // DWIM: This looks kind of like a name/value separator.
480 // Let's let the equals handler have it and break the potential
481 // heading. This is heuristic, but AFAICT the methods for
482 // completely correct disambiguation are very complex.
483 } elseif ( $count > 0 ) {
484 $piece = [
485 'open' => "\n",
486 'close' => "\n",
487 'parts' => [ new PPDPart_Hash( str_repeat( '=', $count ) ) ],
488 'startPos' => $i,
489 'count' => $count ];
490 $stack->push( $piece );
491 $accum =& $stack->getAccum();
492 $stackFlags = $stack->getFlags();
493 if ( isset( $stackFlags['findEquals'] ) ) {
494 $findEquals = $stackFlags['findEquals'];
495 }
496 if ( isset( $stackFlags['findPipe'] ) ) {
497 $findPipe = $stackFlags['findPipe'];
498 }
499 if ( isset( $stackFlags['inHeading'] ) ) {
500 $inHeading = $stackFlags['inHeading'];
501 }
502 $i += $count;
503 }
504 } elseif ( $found == 'line-end' ) {
505 $piece = $stack->top;
506 // A heading must be open, otherwise \n wouldn't have been in the search list
507 // FIXME: Don't use assert()
508 // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.assert
509 assert( $piece->open === "\n" );
510 $part = $piece->getCurrentPart();
511 // Search back through the input to see if it has a proper close.
512 // Do this using the reversed string since the other solutions
513 // (end anchor, etc.) are inefficient.
514 $wsLength = strspn( $revText, " \t", $lengthText - $i );
515 $searchStart = $i - $wsLength;
516 if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
517 // Comment found at line end
518 // Search for equals signs before the comment
519 $searchStart = $part->visualEnd;
520 $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
521 }
522 $count = $piece->count;
523 $equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
524 if ( $equalsLength > 0 ) {
525 if ( $searchStart - $equalsLength == $piece->startPos ) {
526 // This is just a single string of equals signs on its own line
527 // Replicate the doHeadings behavior /={count}(.+)={count}/
528 // First find out how many equals signs there really are (don't stop at 6)
529 $count = $equalsLength;
530 if ( $count < 3 ) {
531 $count = 0;
532 } else {
533 $count = min( 6, intval( ( $count - 1 ) / 2 ) );
534 }
535 } else {
536 $count = min( $equalsLength, $count );
537 }
538 if ( $count > 0 ) {
539 // Normal match, output <h>
540 $element = [ [ 'possible-h',
541 array_merge(
542 [
543 [ '@level', [ $count ] ],
544 [ '@i', [ $headingIndex++ ] ]
545 ],
546 $accum
547 )
548 ] ];
549 } else {
550 // Single equals sign on its own line, count=0
551 $element = $accum;
552 }
553 } else {
554 // No match, no <h>, just pass down the inner text
555 $element = $accum;
556 }
557 // Unwind the stack
558 $stack->pop();
559 $accum =& $stack->getAccum();
560 $stackFlags = $stack->getFlags();
561 if ( isset( $stackFlags['findEquals'] ) ) {
562 $findEquals = $stackFlags['findEquals'];
563 }
564 if ( isset( $stackFlags['findPipe'] ) ) {
565 $findPipe = $stackFlags['findPipe'];
566 }
567 if ( isset( $stackFlags['inHeading'] ) ) {
568 $inHeading = $stackFlags['inHeading'];
569 }
570
571 // Append the result to the enclosing accumulator
572 array_splice( $accum, count( $accum ), 0, $element );
573
574 // Note that we do NOT increment the input pointer.
575 // This is because the closing linebreak could be the opening linebreak of
576 // another heading. Infinite loops are avoided because the next iteration MUST
577 // hit the heading open case above, which unconditionally increments the
578 // input pointer.
579 } elseif ( $found == 'open' ) {
580 # count opening brace characters
581 $curLen = strlen( $curChar );
582 $count = ( $curLen > 1 ) ?
583 # allow the final character to repeat
584 strspn( $text, $curChar[$curLen - 1], $i + 1 ) + 1 :
585 strspn( $text, $curChar, $i );
586
587 $savedPrefix = '';
588 $lineStart = ( $i > 0 && $text[$i - 1] == "\n" );
589
590 if ( $curChar === "-{" && $count > $curLen ) {
591 // -{ => {{ transition because rightmost wins
592 $savedPrefix = '-';
593 $i++;
594 $curChar = '{';
595 $count--;
596 $rule = $this->rules[$curChar];
597 }
598
599 # we need to add to stack only if opening brace count is enough for one of the rules
600 if ( $count >= $rule['min'] ) {
601 # Add it to the stack
602 $piece = [
603 'open' => $curChar,
604 'close' => $rule['end'],
605 'savedPrefix' => $savedPrefix,
606 'count' => $count,
607 'lineStart' => $lineStart,
608 ];
609
610 $stack->push( $piece );
611 $accum =& $stack->getAccum();
612 $stackFlags = $stack->getFlags();
613 if ( isset( $stackFlags['findEquals'] ) ) {
614 $findEquals = $stackFlags['findEquals'];
615 }
616 if ( isset( $stackFlags['findPipe'] ) ) {
617 $findPipe = $stackFlags['findPipe'];
618 }
619 if ( isset( $stackFlags['inHeading'] ) ) {
620 $inHeading = $stackFlags['inHeading'];
621 }
622 } else {
623 # Add literal brace(s)
624 self::addLiteral( $accum, $savedPrefix . str_repeat( $curChar, $count ) );
625 }
626 $i += $count;
627 } elseif ( $found == 'close' ) {
628 $piece = $stack->top;
629 # lets check if there are enough characters for closing brace
630 $maxCount = $piece->count;
631 if ( $piece->close === '}-' && $curChar === '}' ) {
632 $maxCount--; # don't try to match closing '-' as a '}'
633 }
634 $curLen = strlen( $curChar );
635 $count = ( $curLen > 1 ) ? $curLen :
636 strspn( $text, $curChar, $i, $maxCount );
637
638 # check for maximum matching characters (if there are 5 closing
639 # characters, we will probably need only 3 - depending on the rules)
640 $rule = $this->rules[$piece->open];
641 if ( $count > $rule['max'] ) {
642 # The specified maximum exists in the callback array, unless the caller
643 # has made an error
644 $matchingCount = $rule['max'];
645 } else {
646 # Count is less than the maximum
647 # Skip any gaps in the callback array to find the true largest match
648 # Need to use array_key_exists not isset because the callback can be null
649 $matchingCount = $count;
650 while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
651 --$matchingCount;
652 }
653 }
654
655 if ( $matchingCount <= 0 ) {
656 # No matching element found in callback array
657 # Output a literal closing brace and continue
658 $endText = substr( $text, $i, $count );
659 self::addLiteral( $accum, $endText );
660 $i += $count;
661 continue;
662 }
663 $name = $rule['names'][$matchingCount];
664 if ( $name === null ) {
665 // No element, just literal text
666 $endText = substr( $text, $i, $matchingCount );
667 $element = $piece->breakSyntax( $matchingCount );
668 self::addLiteral( $element, $endText );
669 } else {
670 # Create XML element
671 $parts = $piece->parts;
672 $titleAccum = $parts[0]->out;
673 unset( $parts[0] );
674
675 $children = [];
676
677 # The invocation is at the start of the line if lineStart is set in
678 # the stack, and all opening brackets are used up.
679 if ( $maxCount == $matchingCount &&
680 !empty( $piece->lineStart ) &&
681 strlen( $piece->savedPrefix ) == 0 ) {
682 $children[] = [ '@lineStart', [ 1 ] ];
683 }
684 $titleNode = [ 'title', $titleAccum ];
685 $children[] = $titleNode;
686 $argIndex = 1;
687 foreach ( $parts as $part ) {
688 if ( isset( $part->eqpos ) ) {
689 $equalsNode = $part->out[$part->eqpos];
690 $nameNode = [ 'name', array_slice( $part->out, 0, $part->eqpos ) ];
691 $valueNode = [ 'value', array_slice( $part->out, $part->eqpos + 1 ) ];
692 $partNode = [ 'part', [ $nameNode, $equalsNode, $valueNode ] ];
693 $children[] = $partNode;
694 } else {
695 $nameNode = [ 'name', [ [ '@index', [ $argIndex++ ] ] ] ];
696 $valueNode = [ 'value', $part->out ];
697 $partNode = [ 'part', [ $nameNode, $valueNode ] ];
698 $children[] = $partNode;
699 }
700 }
701 $element = [ [ $name, $children ] ];
702 }
703
704 # Advance input pointer
705 $i += $matchingCount;
706
707 # Unwind the stack
708 $stack->pop();
709 $accum =& $stack->getAccum();
710
711 # Re-add the old stack element if it still has unmatched opening characters remaining
712 if ( $matchingCount < $piece->count ) {
713 $piece->parts = [ new PPDPart_Hash ];
714 $piece->count -= $matchingCount;
715 # do we still qualify for any callback with remaining count?
716 $min = $this->rules[$piece->open]['min'];
717 if ( $piece->count >= $min ) {
718 $stack->push( $piece );
719 $accum =& $stack->getAccum();
720 } elseif ( $piece->count == 1 && $piece->open === '{' && $piece->savedPrefix === '-' ) {
721 $piece->savedPrefix = '';
722 $piece->open = '-{';
723 $piece->count = 2;
724 $piece->close = $this->rules[$piece->open]['end'];
725 $stack->push( $piece );
726 $accum =& $stack->getAccum();
727 } else {
728 $s = substr( $piece->open, 0, -1 );
729 $s .= str_repeat(
730 substr( $piece->open, -1 ),
731 $piece->count - strlen( $s )
732 );
733 self::addLiteral( $accum, $piece->savedPrefix . $s );
734 }
735 } elseif ( $piece->savedPrefix !== '' ) {
736 self::addLiteral( $accum, $piece->savedPrefix );
737 }
738
739 $stackFlags = $stack->getFlags();
740 if ( isset( $stackFlags['findEquals'] ) ) {
741 $findEquals = $stackFlags['findEquals'];
742 }
743 if ( isset( $stackFlags['findPipe'] ) ) {
744 $findPipe = $stackFlags['findPipe'];
745 }
746 if ( isset( $stackFlags['inHeading'] ) ) {
747 $inHeading = $stackFlags['inHeading'];
748 }
749
750 # Add XML element to the enclosing accumulator
751 array_splice( $accum, count( $accum ), 0, $element );
752 } elseif ( $found == 'pipe' ) {
753 $findEquals = true; // shortcut for getFlags()
754 $stack->addPart();
755 $accum =& $stack->getAccum();
756 ++$i;
757 } elseif ( $found == 'equals' ) {
758 $findEquals = false; // shortcut for getFlags()
759 $accum[] = [ 'equals', [ '=' ] ];
760 $stack->getCurrentPart()->eqpos = count( $accum ) - 1;
761 ++$i;
762 }
763 }
764
765 # Output any remaining unclosed brackets
766 foreach ( $stack->stack as $piece ) {
767 array_splice( $stack->rootAccum, count( $stack->rootAccum ), 0, $piece->breakSyntax() );
768 }
769
770 # Enable top-level headings
771 foreach ( $stack->rootAccum as &$node ) {
772 if ( is_array( $node ) && $node[PPNode_Hash_Tree::NAME] === 'possible-h' ) {
773 $node[PPNode_Hash_Tree::NAME] = 'h';
774 }
775 }
776
777 $rootStore = [ [ 'root', $stack->rootAccum ] ];
778 $rootNode = new PPNode_Hash_Tree( $rootStore, 0 );
779
780 // Cache
781 $tree = json_encode( $rootStore, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
782 if ( $tree !== false ) {
783 $this->cacheSetTree( $text, $flags, $tree );
784 }
785
786 return $rootNode;
787 }
788
789 private static function addLiteral( array &$accum, $text ) {
790 $n = count( $accum );
791 if ( $n && is_string( $accum[$n - 1] ) ) {
792 $accum[$n - 1] .= $text;
793 } else {
794 $accum[] = $text;
795 }
796 }
797}
798
803// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
804class PPDStack_Hash extends PPDStack {
805
806 public function __construct() {
807 $this->elementClass = PPDStackElement_Hash::class;
808 parent::__construct();
809 $this->rootAccum = [];
810 }
811}
812
816// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
817class PPDStackElement_Hash extends PPDStackElement {
818
819 public function __construct( $data = [] ) {
820 $this->partClass = PPDPart_Hash::class;
821 parent::__construct( $data );
822 }
823
830 public function breakSyntax( $openingCount = false ) {
831 if ( $this->open == "\n" ) {
832 $accum = array_merge( [ $this->savedPrefix ], $this->parts[0]->out );
833 } else {
834 if ( $openingCount === false ) {
835 $openingCount = $this->count;
836 }
837 $s = substr( $this->open, 0, -1 );
838 $s .= str_repeat(
839 substr( $this->open, -1 ),
840 $openingCount - strlen( $s )
841 );
842 $accum = [ $this->savedPrefix . $s ];
843 $lastIndex = 0;
844 $first = true;
845 foreach ( $this->parts as $part ) {
846 if ( $first ) {
847 $first = false;
848 } elseif ( is_string( $accum[$lastIndex] ) ) {
849 $accum[$lastIndex] .= '|';
850 } else {
851 $accum[++$lastIndex] = '|';
852 }
853 foreach ( $part->out as $node ) {
854 if ( is_string( $node ) && is_string( $accum[$lastIndex] ) ) {
855 $accum[$lastIndex] .= $node;
856 } else {
857 $accum[++$lastIndex] = $node;
858 }
859 }
860 }
861 }
862 return $accum;
863 }
864}
865
869// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
870class PPDPart_Hash extends PPDPart {
871
872 public function __construct( $out = '' ) {
873 if ( $out !== '' ) {
874 $accum = [ $out ];
875 } else {
876 $accum = [];
877 }
878 parent::__construct( $accum );
879 }
880}
881
886// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
887class PPFrame_Hash implements PPFrame {
888
892 public $parser;
893
897 public $preprocessor;
898
902 public $title;
903 public $titleCache;
904
909 public $loopCheckHash;
910
915 public $depth;
916
917 private $volatile = false;
918 private $ttl = null;
919
923 protected $childExpansionCache;
924
929 public function __construct( $preprocessor ) {
930 $this->preprocessor = $preprocessor;
931 $this->parser = $preprocessor->parser;
932 $this->title = $this->parser->mTitle;
933 $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
934 $this->loopCheckHash = [];
935 $this->depth = 0;
936 $this->childExpansionCache = [];
937 }
938
949 public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
950 $namedArgs = [];
951 $numberedArgs = [];
952 if ( $title === false ) {
953 $title = $this->title;
954 }
955 if ( $args !== false ) {
956 if ( $args instanceof PPNode_Hash_Array ) {
957 $args = $args->value;
958 } elseif ( !is_array( $args ) ) {
959 throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
960 }
961 foreach ( $args as $arg ) {
962 $bits = $arg->splitArg();
963 if ( $bits['index'] !== '' ) {
964 // Numbered parameter
965 $index = $bits['index'] - $indexOffset;
966 if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
967 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
968 wfEscapeWikiText( $this->title ),
969 wfEscapeWikiText( $title ),
970 wfEscapeWikiText( $index ) )->text() );
971 $this->parser->addTrackingCategory( 'duplicate-args-category' );
972 }
973 $numberedArgs[$index] = $bits['value'];
974 unset( $namedArgs[$index] );
975 } else {
976 // Named parameter
977 $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
978 if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
979 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
980 wfEscapeWikiText( $this->title ),
981 wfEscapeWikiText( $title ),
982 wfEscapeWikiText( $name ) )->text() );
983 $this->parser->addTrackingCategory( 'duplicate-args-category' );
984 }
985 $namedArgs[$name] = $bits['value'];
986 unset( $numberedArgs[$name] );
987 }
988 }
989 }
990 return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
991 }
992
1000 public function cachedExpand( $key, $root, $flags = 0 ) {
1001 // we don't have a parent, so we don't have a cache
1002 return $this->expand( $root, $flags );
1003 }
1004
1011 public function expand( $root, $flags = 0 ) {
1012 static $expansionDepth = 0;
1013 if ( is_string( $root ) ) {
1014 return $root;
1015 }
1016
1017 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
1018 $this->parser->limitationWarn( 'node-count-exceeded',
1019 $this->parser->mPPNodeCount,
1020 $this->parser->mOptions->getMaxPPNodeCount()
1021 );
1022 return '<span class="error">Node-count limit exceeded</span>';
1023 }
1024 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
1025 $this->parser->limitationWarn( 'expansion-depth-exceeded',
1026 $expansionDepth,
1027 $this->parser->mOptions->getMaxPPExpandDepth()
1028 );
1029 return '<span class="error">Expansion depth limit exceeded</span>';
1030 }
1031 ++$expansionDepth;
1032 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
1033 $this->parser->mHighestExpansionDepth = $expansionDepth;
1034 }
1035
1036 $outStack = [ '', '' ];
1037 $iteratorStack = [ false, $root ];
1038 $indexStack = [ 0, 0 ];
1039
1040 while ( count( $iteratorStack ) > 1 ) {
1041 $level = count( $outStack ) - 1;
1042 $iteratorNode =& $iteratorStack[$level];
1043 $out =& $outStack[$level];
1044 $index =& $indexStack[$level];
1045
1046 if ( is_array( $iteratorNode ) ) {
1047 if ( $index >= count( $iteratorNode ) ) {
1048 // All done with this iterator
1049 $iteratorStack[$level] = false;
1050 $contextNode = false;
1051 } else {
1052 $contextNode = $iteratorNode[$index];
1053 $index++;
1054 }
1055 } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
1056 if ( $index >= $iteratorNode->getLength() ) {
1057 // All done with this iterator
1058 $iteratorStack[$level] = false;
1059 $contextNode = false;
1060 } else {
1061 $contextNode = $iteratorNode->item( $index );
1062 $index++;
1063 }
1064 } else {
1065 // Copy to $contextNode and then delete from iterator stack,
1066 // because this is not an iterator but we do have to execute it once
1067 $contextNode = $iteratorStack[$level];
1068 $iteratorStack[$level] = false;
1069 }
1070
1071 $newIterator = false;
1072 $contextName = false;
1073 $contextChildren = false;
1074
1075 if ( $contextNode === false ) {
1076 // nothing to do
1077 } elseif ( is_string( $contextNode ) ) {
1078 $out .= $contextNode;
1079 } elseif ( $contextNode instanceof PPNode_Hash_Array ) {
1080 $newIterator = $contextNode;
1081 } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
1082 // No output
1083 } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
1084 $out .= $contextNode->value;
1085 } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
1086 $contextName = $contextNode->name;
1087 $contextChildren = $contextNode->getRawChildren();
1088 } elseif ( is_array( $contextNode ) ) {
1089 // Node descriptor array
1090 if ( count( $contextNode ) !== 2 ) {
1091 throw new MWException( __METHOD__ .
1092 ': found an array where a node descriptor should be' );
1093 }
1094 list( $contextName, $contextChildren ) = $contextNode;
1095 } else {
1096 throw new MWException( __METHOD__ . ': Invalid parameter type' );
1097 }
1098
1099 // Handle node descriptor array or tree object
1100 if ( $contextName === false ) {
1101 // Not a node, already handled above
1102 } elseif ( $contextName[0] === '@' ) {
1103 // Attribute: no output
1104 } elseif ( $contextName === 'template' ) {
1105 # Double-brace expansion
1106 $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
1107 if ( $flags & PPFrame::NO_TEMPLATES ) {
1108 $newIterator = $this->virtualBracketedImplode(
1109 '{{', '|', '}}',
1110 $bits['title'],
1111 $bits['parts']
1112 );
1113 } else {
1114 $ret = $this->parser->braceSubstitution( $bits, $this );
1115 if ( isset( $ret['object'] ) ) {
1116 $newIterator = $ret['object'];
1117 } else {
1118 $out .= $ret['text'];
1119 }
1120 }
1121 } elseif ( $contextName === 'tplarg' ) {
1122 # Triple-brace expansion
1123 $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
1124 if ( $flags & PPFrame::NO_ARGS ) {
1125 $newIterator = $this->virtualBracketedImplode(
1126 '{{{', '|', '}}}',
1127 $bits['title'],
1128 $bits['parts']
1129 );
1130 } else {
1131 $ret = $this->parser->argSubstitution( $bits, $this );
1132 if ( isset( $ret['object'] ) ) {
1133 $newIterator = $ret['object'];
1134 } else {
1135 $out .= $ret['text'];
1136 }
1137 }
1138 } elseif ( $contextName === 'comment' ) {
1139 # HTML-style comment
1140 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1141 # Not in RECOVER_COMMENTS mode (msgnw) though.
1142 if ( ( $this->parser->ot['html']
1143 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1144 || ( $flags & PPFrame::STRIP_COMMENTS )
1145 ) && !( $flags & PPFrame::RECOVER_COMMENTS )
1146 ) {
1147 $out .= '';
1148 } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1149 # Add a strip marker in PST mode so that pstPass2() can
1150 # run some old-fashioned regexes on the result.
1151 # Not in RECOVER_COMMENTS mode (extractSections) though.
1152 $out .= $this->parser->insertStripItem( $contextChildren[0] );
1153 } else {
1154 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1155 $out .= $contextChildren[0];
1156 }
1157 } elseif ( $contextName === 'ignore' ) {
1158 # Output suppression used by <includeonly> etc.
1159 # OT_WIKI will only respect <ignore> in substed templates.
1160 # The other output types respect it unless NO_IGNORE is set.
1161 # extractSections() sets NO_IGNORE and so never respects it.
1162 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1163 || ( $flags & PPFrame::NO_IGNORE )
1164 ) {
1165 $out .= $contextChildren[0];
1166 } else {
1167 // $out .= '';
1168 }
1169 } elseif ( $contextName === 'ext' ) {
1170 # Extension tag
1171 $bits = PPNode_Hash_Tree::splitRawExt( $contextChildren ) +
1172 [ 'attr' => null, 'inner' => null, 'close' => null ];
1173 if ( $flags & PPFrame::NO_TAGS ) {
1174 $s = '<' . $bits['name']->getFirstChild()->value;
1175 if ( $bits['attr'] ) {
1176 $s .= $bits['attr']->getFirstChild()->value;
1177 }
1178 if ( $bits['inner'] ) {
1179 $s .= '>' . $bits['inner']->getFirstChild()->value;
1180 if ( $bits['close'] ) {
1181 $s .= $bits['close']->getFirstChild()->value;
1182 }
1183 } else {
1184 $s .= '/>';
1185 }
1186 $out .= $s;
1187 } else {
1188 $out .= $this->parser->extensionSubstitution( $bits, $this );
1189 }
1190 } elseif ( $contextName === 'h' ) {
1191 # Heading
1192 if ( $this->parser->ot['html'] ) {
1193 # Expand immediately and insert heading index marker
1194 $s = $this->expand( $contextChildren, $flags );
1195 $bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
1196 $titleText = $this->title->getPrefixedDBkey();
1197 $this->parser->mHeadings[] = [ $titleText, $bits['i'] ];
1198 $serial = count( $this->parser->mHeadings ) - 1;
1199 $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
1200 $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
1201 $this->parser->mStripState->addGeneral( $marker, '' );
1202 $out .= $s;
1203 } else {
1204 # Expand in virtual stack
1205 $newIterator = $contextChildren;
1206 }
1207 } else {
1208 # Generic recursive expansion
1209 $newIterator = $contextChildren;
1210 }
1211
1212 if ( $newIterator !== false ) {
1213 $outStack[] = '';
1214 $iteratorStack[] = $newIterator;
1215 $indexStack[] = 0;
1216 } elseif ( $iteratorStack[$level] === false ) {
1217 // Return accumulated value to parent
1218 // With tail recursion
1219 while ( $iteratorStack[$level] === false && $level > 0 ) {
1220 $outStack[$level - 1] .= $out;
1221 array_pop( $outStack );
1222 array_pop( $iteratorStack );
1223 array_pop( $indexStack );
1224 $level--;
1225 }
1226 }
1227 }
1228 --$expansionDepth;
1229 return $outStack[0];
1230 }
1231
1238 public function implodeWithFlags( $sep, $flags /*, ... */ ) {
1239 $args = array_slice( func_get_args(), 2 );
1240
1241 $first = true;
1242 $s = '';
1243 foreach ( $args as $root ) {
1244 if ( $root instanceof PPNode_Hash_Array ) {
1245 $root = $root->value;
1246 }
1247 if ( !is_array( $root ) ) {
1248 $root = [ $root ];
1249 }
1250 foreach ( $root as $node ) {
1251 if ( $first ) {
1252 $first = false;
1253 } else {
1254 $s .= $sep;
1255 }
1256 $s .= $this->expand( $node, $flags );
1257 }
1258 }
1259 return $s;
1260 }
1261
1269 public function implode( $sep /*, ... */ ) {
1270 $args = array_slice( func_get_args(), 1 );
1271
1272 $first = true;
1273 $s = '';
1274 foreach ( $args as $root ) {
1275 if ( $root instanceof PPNode_Hash_Array ) {
1276 $root = $root->value;
1277 }
1278 if ( !is_array( $root ) ) {
1279 $root = [ $root ];
1280 }
1281 foreach ( $root as $node ) {
1282 if ( $first ) {
1283 $first = false;
1284 } else {
1285 $s .= $sep;
1286 }
1287 $s .= $this->expand( $node );
1288 }
1289 }
1290 return $s;
1291 }
1292
1301 public function virtualImplode( $sep /*, ... */ ) {
1302 $args = array_slice( func_get_args(), 1 );
1303 $out = [];
1304 $first = true;
1305
1306 foreach ( $args as $root ) {
1307 if ( $root instanceof PPNode_Hash_Array ) {
1308 $root = $root->value;
1309 }
1310 if ( !is_array( $root ) ) {
1311 $root = [ $root ];
1312 }
1313 foreach ( $root as $node ) {
1314 if ( $first ) {
1315 $first = false;
1316 } else {
1317 $out[] = $sep;
1318 }
1319 $out[] = $node;
1320 }
1321 }
1322 return new PPNode_Hash_Array( $out );
1323 }
1324
1334 public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1335 $args = array_slice( func_get_args(), 3 );
1336 $out = [ $start ];
1337 $first = true;
1338
1339 foreach ( $args as $root ) {
1340 if ( $root instanceof PPNode_Hash_Array ) {
1341 $root = $root->value;
1342 }
1343 if ( !is_array( $root ) ) {
1344 $root = [ $root ];
1345 }
1346 foreach ( $root as $node ) {
1347 if ( $first ) {
1348 $first = false;
1349 } else {
1350 $out[] = $sep;
1351 }
1352 $out[] = $node;
1353 }
1354 }
1355 $out[] = $end;
1356 return new PPNode_Hash_Array( $out );
1357 }
1358
1359 public function __toString() {
1360 return 'frame{}';
1361 }
1362
1367 public function getPDBK( $level = false ) {
1368 if ( $level === false ) {
1369 return $this->title->getPrefixedDBkey();
1370 } else {
1371 return $this->titleCache[$level] ?? false;
1372 }
1373 }
1374
1378 public function getArguments() {
1379 return [];
1380 }
1381
1385 public function getNumberedArguments() {
1386 return [];
1387 }
1388
1392 public function getNamedArguments() {
1393 return [];
1394 }
1395
1401 public function isEmpty() {
1402 return true;
1403 }
1404
1409 public function getArgument( $name ) {
1410 return false;
1411 }
1412
1420 public function loopCheck( $title ) {
1421 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1422 }
1423
1429 public function isTemplate() {
1430 return false;
1431 }
1432
1438 public function getTitle() {
1439 return $this->title;
1440 }
1441
1447 public function setVolatile( $flag = true ) {
1448 $this->volatile = $flag;
1449 }
1450
1456 public function isVolatile() {
1457 return $this->volatile;
1458 }
1459
1465 public function setTTL( $ttl ) {
1466 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1467 $this->ttl = $ttl;
1468 }
1469 }
1470
1476 public function getTTL() {
1477 return $this->ttl;
1478 }
1479}
1480
1485// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1486class PPTemplateFrame_Hash extends PPFrame_Hash {
1487
1488 public $numberedArgs, $namedArgs, $parent;
1489 public $numberedExpansionCache, $namedExpansionCache;
1490
1498 public function __construct( $preprocessor, $parent = false, $numberedArgs = [],
1499 $namedArgs = [], $title = false
1500 ) {
1501 parent::__construct( $preprocessor );
1502
1503 $this->parent = $parent;
1504 $this->numberedArgs = $numberedArgs;
1505 $this->namedArgs = $namedArgs;
1506 $this->title = $title;
1507 $pdbk = $title ? $title->getPrefixedDBkey() : false;
1508 $this->titleCache = $parent->titleCache;
1509 $this->titleCache[] = $pdbk;
1510 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1511 if ( $pdbk !== false ) {
1512 $this->loopCheckHash[$pdbk] = true;
1513 }
1514 $this->depth = $parent->depth + 1;
1515 $this->numberedExpansionCache = $this->namedExpansionCache = [];
1516 }
1517
1518 public function __toString() {
1519 $s = 'tplframe{';
1520 $first = true;
1521 $args = $this->numberedArgs + $this->namedArgs;
1522 foreach ( $args as $name => $value ) {
1523 if ( $first ) {
1524 $first = false;
1525 } else {
1526 $s .= ', ';
1527 }
1528 $s .= "\"$name\":\"" .
1529 str_replace( '"', '\\"', $value->__toString() ) . '"';
1530 }
1531 $s .= '}';
1532 return $s;
1533 }
1534
1542 public function cachedExpand( $key, $root, $flags = 0 ) {
1543 if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1544 return $this->parent->childExpansionCache[$key];
1545 }
1546 $retval = $this->expand( $root, $flags );
1547 if ( !$this->isVolatile() ) {
1548 $this->parent->childExpansionCache[$key] = $retval;
1549 }
1550 return $retval;
1551 }
1552
1558 public function isEmpty() {
1559 return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1560 }
1561
1565 public function getArguments() {
1566 $arguments = [];
1567 foreach ( array_merge(
1568 array_keys( $this->numberedArgs ),
1569 array_keys( $this->namedArgs ) ) as $key ) {
1570 $arguments[$key] = $this->getArgument( $key );
1571 }
1572 return $arguments;
1573 }
1574
1578 public function getNumberedArguments() {
1579 $arguments = [];
1580 foreach ( array_keys( $this->numberedArgs ) as $key ) {
1581 $arguments[$key] = $this->getArgument( $key );
1582 }
1583 return $arguments;
1584 }
1585
1589 public function getNamedArguments() {
1590 $arguments = [];
1591 foreach ( array_keys( $this->namedArgs ) as $key ) {
1592 $arguments[$key] = $this->getArgument( $key );
1593 }
1594 return $arguments;
1595 }
1596
1601 public function getNumberedArgument( $index ) {
1602 if ( !isset( $this->numberedArgs[$index] ) ) {
1603 return false;
1604 }
1605 if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1606 # No trimming for unnamed arguments
1607 $this->numberedExpansionCache[$index] = $this->parent->expand(
1608 $this->numberedArgs[$index],
1609 PPFrame::STRIP_COMMENTS
1610 );
1611 }
1612 return $this->numberedExpansionCache[$index];
1613 }
1614
1619 public function getNamedArgument( $name ) {
1620 if ( !isset( $this->namedArgs[$name] ) ) {
1621 return false;
1622 }
1623 if ( !isset( $this->namedExpansionCache[$name] ) ) {
1624 # Trim named arguments post-expand, for backwards compatibility
1625 $this->namedExpansionCache[$name] = trim(
1626 $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1627 }
1628 return $this->namedExpansionCache[$name];
1629 }
1630
1635 public function getArgument( $name ) {
1636 $text = $this->getNumberedArgument( $name );
1637 if ( $text === false ) {
1638 $text = $this->getNamedArgument( $name );
1639 }
1640 return $text;
1641 }
1642
1648 public function isTemplate() {
1649 return true;
1650 }
1651
1652 public function setVolatile( $flag = true ) {
1653 parent::setVolatile( $flag );
1654 $this->parent->setVolatile( $flag );
1655 }
1656
1657 public function setTTL( $ttl ) {
1658 parent::setTTL( $ttl );
1659 $this->parent->setTTL( $ttl );
1660 }
1661}
1662
1667// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1668class PPCustomFrame_Hash extends PPFrame_Hash {
1669
1670 public $args;
1671
1672 public function __construct( $preprocessor, $args ) {
1673 parent::__construct( $preprocessor );
1674 $this->args = $args;
1675 }
1676
1677 public function __toString() {
1678 $s = 'cstmframe{';
1679 $first = true;
1680 foreach ( $this->args as $name => $value ) {
1681 if ( $first ) {
1682 $first = false;
1683 } else {
1684 $s .= ', ';
1685 }
1686 $s .= "\"$name\":\"" .
1687 str_replace( '"', '\\"', $value->__toString() ) . '"';
1688 }
1689 $s .= '}';
1690 return $s;
1691 }
1692
1696 public function isEmpty() {
1697 return !count( $this->args );
1698 }
1699
1704 public function getArgument( $index ) {
1705 if ( !isset( $this->args[$index] ) ) {
1706 return false;
1707 }
1708 return $this->args[$index];
1709 }
1710
1711 public function getArguments() {
1712 return $this->args;
1713 }
1714}
1715
1719// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1720class PPNode_Hash_Tree implements PPNode {
1721
1722 public $name;
1723
1730
1734 private $store;
1735
1739 private $index;
1740
1745 const NAME = 0;
1746
1751 const CHILDREN = 1;
1752
1760 public function __construct( array $store, $index ) {
1761 $this->store = $store;
1762 $this->index = $index;
1763 list( $this->name, $this->rawChildren ) = $this->store[$index];
1764 }
1765
1774 public static function factory( array $store, $index ) {
1775 if ( !isset( $store[$index] ) ) {
1776 return false;
1777 }
1778
1779 $descriptor = $store[$index];
1780 if ( is_string( $descriptor ) ) {
1781 $class = PPNode_Hash_Text::class;
1782 } elseif ( is_array( $descriptor ) ) {
1783 if ( $descriptor[self::NAME][0] === '@' ) {
1784 $class = PPNode_Hash_Attr::class;
1785 } else {
1786 $class = self::class;
1787 }
1788 } else {
1789 throw new MWException( __METHOD__ . ': invalid node descriptor' );
1790 }
1791 return new $class( $store, $index );
1792 }
1793
1797 public function __toString() {
1798 $inner = '';
1799 $attribs = '';
1800 for ( $node = $this->getFirstChild(); $node; $node = $node->getNextSibling() ) {
1801 if ( $node instanceof PPNode_Hash_Attr ) {
1802 $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
1803 } else {
1804 $inner .= $node->__toString();
1805 }
1806 }
1807 if ( $inner === '' ) {
1808 return "<{$this->name}$attribs/>";
1809 } else {
1810 return "<{$this->name}$attribs>$inner</{$this->name}>";
1811 }
1812 }
1813
1817 public function getChildren() {
1818 $children = [];
1819 foreach ( $this->rawChildren as $i => $child ) {
1820 $children[] = self::factory( $this->rawChildren, $i );
1821 }
1822 return new PPNode_Hash_Array( $children );
1823 }
1824
1832 public function getFirstChild() {
1833 if ( !isset( $this->rawChildren[0] ) ) {
1834 return false;
1835 } else {
1836 return self::factory( $this->rawChildren, 0 );
1837 }
1838 }
1839
1847 public function getNextSibling() {
1848 return self::factory( $this->store, $this->index + 1 );
1849 }
1850
1857 public function getChildrenOfType( $name ) {
1858 $children = [];
1859 foreach ( $this->rawChildren as $i => $child ) {
1860 if ( is_array( $child ) && $child[self::NAME] === $name ) {
1861 $children[] = self::factory( $this->rawChildren, $i );
1862 }
1863 }
1864 return new PPNode_Hash_Array( $children );
1865 }
1866
1871 public function getRawChildren() {
1872 return $this->rawChildren;
1873 }
1874
1878 public function getLength() {
1879 return false;
1880 }
1881
1886 public function item( $i ) {
1887 return false;
1888 }
1889
1893 public function getName() {
1894 return $this->name;
1895 }
1896
1906 public function splitArg() {
1907 return self::splitRawArg( $this->rawChildren );
1908 }
1909
1915 public static function splitRawArg( array $children ) {
1916 $bits = [];
1917 foreach ( $children as $i => $child ) {
1918 if ( !is_array( $child ) ) {
1919 continue;
1920 }
1921 if ( $child[self::NAME] === 'name' ) {
1922 $bits['name'] = new self( $children, $i );
1923 if ( isset( $child[self::CHILDREN][0][self::NAME] )
1924 && $child[self::CHILDREN][0][self::NAME] === '@index'
1925 ) {
1926 $bits['index'] = $child[self::CHILDREN][0][self::CHILDREN][0];
1927 }
1928 } elseif ( $child[self::NAME] === 'value' ) {
1929 $bits['value'] = new self( $children, $i );
1930 }
1931 }
1932
1933 if ( !isset( $bits['name'] ) ) {
1934 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
1935 }
1936 if ( !isset( $bits['index'] ) ) {
1937 $bits['index'] = '';
1938 }
1939 return $bits;
1940 }
1941
1949 public function splitExt() {
1950 return self::splitRawExt( $this->rawChildren );
1951 }
1952
1958 public static function splitRawExt( array $children ) {
1959 $bits = [];
1960 foreach ( $children as $i => $child ) {
1961 if ( !is_array( $child ) ) {
1962 continue;
1963 }
1964 switch ( $child[self::NAME] ) {
1965 case 'name':
1966 $bits['name'] = new self( $children, $i );
1967 break;
1968 case 'attr':
1969 $bits['attr'] = new self( $children, $i );
1970 break;
1971 case 'inner':
1972 $bits['inner'] = new self( $children, $i );
1973 break;
1974 case 'close':
1975 $bits['close'] = new self( $children, $i );
1976 break;
1977 }
1978 }
1979 if ( !isset( $bits['name'] ) ) {
1980 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
1981 }
1982 return $bits;
1983 }
1984
1991 public function splitHeading() {
1992 if ( $this->name !== 'h' ) {
1993 throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1994 }
1995 return self::splitRawHeading( $this->rawChildren );
1996 }
1997
2003 public static function splitRawHeading( array $children ) {
2004 $bits = [];
2005 foreach ( $children as $i => $child ) {
2006 if ( !is_array( $child ) ) {
2007 continue;
2008 }
2009 if ( $child[self::NAME] === '@i' ) {
2010 $bits['i'] = $child[self::CHILDREN][0];
2011 } elseif ( $child[self::NAME] === '@level' ) {
2012 $bits['level'] = $child[self::CHILDREN][0];
2013 }
2014 }
2015 if ( !isset( $bits['i'] ) ) {
2016 throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
2017 }
2018 return $bits;
2019 }
2020
2027 public function splitTemplate() {
2028 return self::splitRawTemplate( $this->rawChildren );
2029 }
2030
2036 public static function splitRawTemplate( array $children ) {
2037 $parts = [];
2038 $bits = [ 'lineStart' => '' ];
2039 foreach ( $children as $i => $child ) {
2040 if ( !is_array( $child ) ) {
2041 continue;
2042 }
2043 switch ( $child[self::NAME] ) {
2044 case 'title':
2045 $bits['title'] = new self( $children, $i );
2046 break;
2047 case 'part':
2048 $parts[] = new self( $children, $i );
2049 break;
2050 case '@lineStart':
2051 $bits['lineStart'] = '1';
2052 break;
2053 }
2054 }
2055 if ( !isset( $bits['title'] ) ) {
2056 throw new MWException( 'Invalid node passed to ' . __METHOD__ );
2057 }
2058 $bits['parts'] = new PPNode_Hash_Array( $parts );
2059 return $bits;
2060 }
2061}
2062
2066// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
2067class PPNode_Hash_Text implements PPNode {
2068
2069 public $value;
2070 private $store, $index;
2071
2079 public function __construct( array $store, $index ) {
2080 $this->value = $store[$index];
2081 if ( !is_scalar( $this->value ) ) {
2082 throw new MWException( __CLASS__ . ' given object instead of string' );
2083 }
2084 $this->store = $store;
2085 $this->index = $index;
2086 }
2087
2088 public function __toString() {
2089 return htmlspecialchars( $this->value );
2090 }
2091
2092 public function getNextSibling() {
2093 return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
2094 }
2095
2096 public function getChildren() {
2097 return false;
2098 }
2099
2100 public function getFirstChild() {
2101 return false;
2102 }
2103
2104 public function getChildrenOfType( $name ) {
2105 return false;
2106 }
2107
2108 public function getLength() {
2109 return false;
2110 }
2111
2112 public function item( $i ) {
2113 return false;
2114 }
2115
2116 public function getName() {
2117 return '#text';
2118 }
2119
2120 public function splitArg() {
2121 throw new MWException( __METHOD__ . ': not supported' );
2122 }
2123
2124 public function splitExt() {
2125 throw new MWException( __METHOD__ . ': not supported' );
2126 }
2127
2128 public function splitHeading() {
2129 throw new MWException( __METHOD__ . ': not supported' );
2130 }
2131}
2132
2136// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
2137class PPNode_Hash_Array implements PPNode {
2138
2139 public $value;
2140
2141 public function __construct( $value ) {
2142 $this->value = $value;
2143 }
2144
2145 public function __toString() {
2146 return var_export( $this, true );
2147 }
2148
2149 public function getLength() {
2150 return count( $this->value );
2151 }
2152
2153 public function item( $i ) {
2154 return $this->value[$i];
2155 }
2156
2157 public function getName() {
2158 return '#nodelist';
2159 }
2160
2161 public function getNextSibling() {
2162 return false;
2163 }
2164
2165 public function getChildren() {
2166 return false;
2167 }
2168
2169 public function getFirstChild() {
2170 return false;
2171 }
2172
2173 public function getChildrenOfType( $name ) {
2174 return false;
2175 }
2176
2177 public function splitArg() {
2178 throw new MWException( __METHOD__ . ': not supported' );
2179 }
2180
2181 public function splitExt() {
2182 throw new MWException( __METHOD__ . ': not supported' );
2183 }
2184
2185 public function splitHeading() {
2186 throw new MWException( __METHOD__ . ': not supported' );
2187 }
2188}
2189
2193// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
2194class PPNode_Hash_Attr implements PPNode {
2195
2196 public $name, $value;
2197 private $store, $index;
2198
2206 public function __construct( array $store, $index ) {
2207 $descriptor = $store[$index];
2208 if ( $descriptor[PPNode_Hash_Tree::NAME][0] !== '@' ) {
2209 throw new MWException( __METHOD__ . ': invalid name in attribute descriptor' );
2210 }
2211 $this->name = substr( $descriptor[PPNode_Hash_Tree::NAME], 1 );
2212 $this->value = $descriptor[PPNode_Hash_Tree::CHILDREN][0];
2213 $this->store = $store;
2214 $this->index = $index;
2215 }
2216
2217 public function __toString() {
2218 return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
2219 }
2220
2221 public function getName() {
2222 return $this->name;
2223 }
2224
2225 public function getNextSibling() {
2226 return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
2227 }
2228
2229 public function getChildren() {
2230 return false;
2231 }
2232
2233 public function getFirstChild() {
2234 return false;
2235 }
2236
2237 public function getChildrenOfType( $name ) {
2238 return false;
2239 }
2240
2241 public function getLength() {
2242 return false;
2243 }
2244
2245 public function item( $i ) {
2246 return false;
2247 }
2248
2249 public function splitArg() {
2250 throw new MWException( __METHOD__ . ': not supported' );
2251 }
2252
2253 public function splitExt() {
2254 throw new MWException( __METHOD__ . ': not supported' );
2255 }
2256
2257 public function splitHeading() {
2258 throw new MWException( __METHOD__ . ': not supported' );
2259 }
2260}
within a display generated by the Derivative if and wherever such third party notices normally appear The contents of the NOTICE file are for informational purposes only and do not modify the License You may add Your own attribution notices within Derivative Works that You alongside or as an addendum to the NOTICE text from the provided that such additional attribution notices cannot be construed as modifying the License You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for or distribution of Your or for any such Derivative Works as a provided Your and distribution of the Work otherwise complies with the conditions stated in this License Submission of Contributions Unless You explicitly state any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this without any additional terms or conditions Notwithstanding the nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions Trademarks This License does not grant permission to use the trade names
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
$wgDisableLangConversion
Whether to enable language variant conversion.
if( $line===false) $args
Definition cdb.php:64
MediaWiki exception.
Expansion frame with custom arguments.
Stack class to help Preprocessor::preprocessToObj()
An expansion frame, used as a context to expand the result of preprocessToObj()
splitArg()
Split a "<part>" node into an associative array containing: name PPNode name index String index value...
getName()
Get the name of this node.
getNextSibling()
Get the next sibling of any node.
getLength()
Returns the length of the array, or false if this is not an array-type node.
splitHeading()
Split an "<h>" node.
getChildrenOfType( $name)
Get all children of this tree node which have a given name.
getFirstChild()
Get the first child of a tree node.
getChildren()
Get an array-type node containing the children of this node.
item( $i)
Returns an item of an array-type node.
splitExt()
Split an "<ext>" node into an associative array containing name, attr, inner and close All values in ...
getChildren()
Get an array-type node containing the children of this node.
splitExt()
Split an "<ext>" node into an associative array containing name, attr, inner and close All values in ...
splitArg()
Split a "<part>" node into an associative array containing: name PPNode name index String index value...
__construct(array $store, $index)
Construct an object using the data from $store[$index].
item( $i)
Returns an item of an array-type node.
getChildrenOfType( $name)
Get all children of this tree node which have a given name.
splitHeading()
Split an "<h>" node.
getName()
Get the name of this node.
getLength()
Returns the length of the array, or false if this is not an array-type node.
getFirstChild()
Get the first child of a tree node.
getNextSibling()
Get the next sibling of any node.
getChildren()
Get an array-type node containing the children of this node.
getChildrenOfType( $name)
Get all children of this tree node which have a given name.
__construct(array $store, $index)
Construct an object using the data from $store[$index].
getLength()
Returns the length of the array, or false if this is not an array-type node.
splitHeading()
Split an "<h>" node.
getFirstChild()
Get the first child of a tree node.
getNextSibling()
Get the next sibling of any node.
splitArg()
Split a "<part>" node into an associative array containing: name PPNode name index String index value...
getName()
Get the name of this node.
item( $i)
Returns an item of an array-type node.
splitExt()
Split an "<ext>" node into an associative array containing name, attr, inner and close All values in ...
static factory(array $store, $index)
Construct an appropriate PPNode_Hash_* object with a class that depends on what is at the relevant st...
splitExt()
Split an "<ext>" node into an associative array containing name, attr, inner and close All values in ...
static splitRawArg(array $children)
Like splitArg() but for a raw child array.
splitArg()
Split a "<part>" node into an associative array containing:
$index
The index into $this->store which contains the descriptor of this node.
getNextSibling()
Get the next sibling, or false if there is none.
static splitRawTemplate(array $children)
Like splitTemplate() but for a raw child array.
splitHeading()
Split an "<h>" node.
splitTemplate()
Split a "<template>" or "<tplarg>" node.
static splitRawHeading(array $children)
Like splitHeading() but for a raw child array.
$store
The store array for the siblings of this node, including this node itself.
static splitRawExt(array $children)
Like splitExt() but for a raw child array.
__construct(array $store, $index)
Construct an object using the data from $store[$index].
getChildrenOfType( $name)
Get an array of the children with a given node name.
__toString()
Convert a node to XML, for debugging.
const CHILDREN
The offset of the child list within descriptors, used in some places for readability.
$rawChildren
The store array for children of this node.
getFirstChild()
Get the first child, or false if there is none.
getRawChildren()
Get the raw child array.
const NAME
The offset of the name within descriptors, used in some places for readability.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:68
Differences from DOM schema:
static addLiteral(array &$accum, $text)
preprocessToObj( $text, $flags=0)
Preprocess some wikitext and return the document tree.
cacheGetTree( $text, $flags)
Attempt to load a precomputed document tree for some given wikitext from the cache.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same so they can t rely on Unix and must forbid reads to even standard directories like tmp lest users read each others files We cannot assume that the user has the ability to install or run any programs not written as web accessible PHP scripts Since anything that works on cheap shared hosting will work if you have shell or root access MediaWiki s design is based around catering to the lowest common denominator Although we support higher end setups as the way many things work by default is tailored toward shared hosting These defaults are unconventional from the point of view of and they certainly aren t ideal for someone who s installing MediaWiki as root
in this case you re responsible for computing and outputting the entire conflict part
Definition hooks.txt:1462
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition hooks.txt:2063
and how to run hooks for an and one after Each event has a name
Definition hooks.txt:12
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to and or sell copies of the and to permit persons to whom the Software is furnished to do so
Definition LICENSE.txt:13
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
There are three types of nodes:
Prior to maintenance scripts were a hodgepodge of code that had no cohesion or formal method of action Beginning maintenance scripts have been cleaned up to use a unified class Directory structure How to run a script How to write your own DIRECTORY STRUCTURE The maintenance directory of a MediaWiki installation contains several all of which have unique purposes HOW TO RUN A SCRIPT Ridiculously just call php someScript php that s in the top level maintenance directory if not default wiki
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
title
This document describes the state of Postgres support in and is fairly well maintained The main code is very well while extensions are very hit and miss it is probably the most supported database after MySQL Much of the work in making MediaWiki database agnostic came about through the work of creating Postgres as and are nearing end of but without copying over all the usage comments General notes on the but these can almost always be programmed around *Although Postgres has a true BOOLEAN type
Definition postgres.txt:36
MediaWiki s SiteStore can be cached and stored in a flat in a json format If the SiteStore is frequently the file cache may provide a performance benefit over a database store
Definition sitescache.txt:4