MediaWiki REL1_33
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 return $this->args[$index] ?? false;
1706 }
1707
1708 public function getArguments() {
1709 return $this->args;
1710 }
1711}
1712
1716// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1717class PPNode_Hash_Tree implements PPNode {
1718
1719 public $name;
1720
1727
1731 private $store;
1732
1736 private $index;
1737
1742 const NAME = 0;
1743
1748 const CHILDREN = 1;
1749
1757 public function __construct( array $store, $index ) {
1758 $this->store = $store;
1759 $this->index = $index;
1760 list( $this->name, $this->rawChildren ) = $this->store[$index];
1761 }
1762
1771 public static function factory( array $store, $index ) {
1772 if ( !isset( $store[$index] ) ) {
1773 return false;
1774 }
1775
1776 $descriptor = $store[$index];
1777 if ( is_string( $descriptor ) ) {
1778 $class = PPNode_Hash_Text::class;
1779 } elseif ( is_array( $descriptor ) ) {
1780 if ( $descriptor[self::NAME][0] === '@' ) {
1781 $class = PPNode_Hash_Attr::class;
1782 } else {
1783 $class = self::class;
1784 }
1785 } else {
1786 throw new MWException( __METHOD__ . ': invalid node descriptor' );
1787 }
1788 return new $class( $store, $index );
1789 }
1790
1794 public function __toString() {
1795 $inner = '';
1796 $attribs = '';
1797 for ( $node = $this->getFirstChild(); $node; $node = $node->getNextSibling() ) {
1798 if ( $node instanceof PPNode_Hash_Attr ) {
1799 $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
1800 } else {
1801 $inner .= $node->__toString();
1802 }
1803 }
1804 if ( $inner === '' ) {
1805 return "<{$this->name}$attribs/>";
1806 } else {
1807 return "<{$this->name}$attribs>$inner</{$this->name}>";
1808 }
1809 }
1810
1814 public function getChildren() {
1815 $children = [];
1816 foreach ( $this->rawChildren as $i => $child ) {
1817 $children[] = self::factory( $this->rawChildren, $i );
1818 }
1819 return new PPNode_Hash_Array( $children );
1820 }
1821
1829 public function getFirstChild() {
1830 if ( !isset( $this->rawChildren[0] ) ) {
1831 return false;
1832 } else {
1833 return self::factory( $this->rawChildren, 0 );
1834 }
1835 }
1836
1844 public function getNextSibling() {
1845 return self::factory( $this->store, $this->index + 1 );
1846 }
1847
1854 public function getChildrenOfType( $name ) {
1855 $children = [];
1856 foreach ( $this->rawChildren as $i => $child ) {
1857 if ( is_array( $child ) && $child[self::NAME] === $name ) {
1858 $children[] = self::factory( $this->rawChildren, $i );
1859 }
1860 }
1861 return new PPNode_Hash_Array( $children );
1862 }
1863
1868 public function getRawChildren() {
1869 return $this->rawChildren;
1870 }
1871
1875 public function getLength() {
1876 return false;
1877 }
1878
1883 public function item( $i ) {
1884 return false;
1885 }
1886
1890 public function getName() {
1891 return $this->name;
1892 }
1893
1903 public function splitArg() {
1904 return self::splitRawArg( $this->rawChildren );
1905 }
1906
1912 public static function splitRawArg( array $children ) {
1913 $bits = [];
1914 foreach ( $children as $i => $child ) {
1915 if ( !is_array( $child ) ) {
1916 continue;
1917 }
1918 if ( $child[self::NAME] === 'name' ) {
1919 $bits['name'] = new self( $children, $i );
1920 if ( isset( $child[self::CHILDREN][0][self::NAME] )
1921 && $child[self::CHILDREN][0][self::NAME] === '@index'
1922 ) {
1923 $bits['index'] = $child[self::CHILDREN][0][self::CHILDREN][0];
1924 }
1925 } elseif ( $child[self::NAME] === 'value' ) {
1926 $bits['value'] = new self( $children, $i );
1927 }
1928 }
1929
1930 if ( !isset( $bits['name'] ) ) {
1931 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
1932 }
1933 if ( !isset( $bits['index'] ) ) {
1934 $bits['index'] = '';
1935 }
1936 return $bits;
1937 }
1938
1946 public function splitExt() {
1947 return self::splitRawExt( $this->rawChildren );
1948 }
1949
1955 public static function splitRawExt( array $children ) {
1956 $bits = [];
1957 foreach ( $children as $i => $child ) {
1958 if ( !is_array( $child ) ) {
1959 continue;
1960 }
1961 switch ( $child[self::NAME] ) {
1962 case 'name':
1963 $bits['name'] = new self( $children, $i );
1964 break;
1965 case 'attr':
1966 $bits['attr'] = new self( $children, $i );
1967 break;
1968 case 'inner':
1969 $bits['inner'] = new self( $children, $i );
1970 break;
1971 case 'close':
1972 $bits['close'] = new self( $children, $i );
1973 break;
1974 }
1975 }
1976 if ( !isset( $bits['name'] ) ) {
1977 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
1978 }
1979 return $bits;
1980 }
1981
1988 public function splitHeading() {
1989 if ( $this->name !== 'h' ) {
1990 throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1991 }
1992 return self::splitRawHeading( $this->rawChildren );
1993 }
1994
2000 public static function splitRawHeading( array $children ) {
2001 $bits = [];
2002 foreach ( $children as $i => $child ) {
2003 if ( !is_array( $child ) ) {
2004 continue;
2005 }
2006 if ( $child[self::NAME] === '@i' ) {
2007 $bits['i'] = $child[self::CHILDREN][0];
2008 } elseif ( $child[self::NAME] === '@level' ) {
2009 $bits['level'] = $child[self::CHILDREN][0];
2010 }
2011 }
2012 if ( !isset( $bits['i'] ) ) {
2013 throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
2014 }
2015 return $bits;
2016 }
2017
2024 public function splitTemplate() {
2025 return self::splitRawTemplate( $this->rawChildren );
2026 }
2027
2033 public static function splitRawTemplate( array $children ) {
2034 $parts = [];
2035 $bits = [ 'lineStart' => '' ];
2036 foreach ( $children as $i => $child ) {
2037 if ( !is_array( $child ) ) {
2038 continue;
2039 }
2040 switch ( $child[self::NAME] ) {
2041 case 'title':
2042 $bits['title'] = new self( $children, $i );
2043 break;
2044 case 'part':
2045 $parts[] = new self( $children, $i );
2046 break;
2047 case '@lineStart':
2048 $bits['lineStart'] = '1';
2049 break;
2050 }
2051 }
2052 if ( !isset( $bits['title'] ) ) {
2053 throw new MWException( 'Invalid node passed to ' . __METHOD__ );
2054 }
2055 $bits['parts'] = new PPNode_Hash_Array( $parts );
2056 return $bits;
2057 }
2058}
2059
2063// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
2064class PPNode_Hash_Text implements PPNode {
2065
2066 public $value;
2067 private $store, $index;
2068
2076 public function __construct( array $store, $index ) {
2077 $this->value = $store[$index];
2078 if ( !is_scalar( $this->value ) ) {
2079 throw new MWException( __CLASS__ . ' given object instead of string' );
2080 }
2081 $this->store = $store;
2082 $this->index = $index;
2083 }
2084
2085 public function __toString() {
2086 return htmlspecialchars( $this->value );
2087 }
2088
2089 public function getNextSibling() {
2090 return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
2091 }
2092
2093 public function getChildren() {
2094 return false;
2095 }
2096
2097 public function getFirstChild() {
2098 return false;
2099 }
2100
2101 public function getChildrenOfType( $name ) {
2102 return false;
2103 }
2104
2105 public function getLength() {
2106 return false;
2107 }
2108
2109 public function item( $i ) {
2110 return false;
2111 }
2112
2113 public function getName() {
2114 return '#text';
2115 }
2116
2117 public function splitArg() {
2118 throw new MWException( __METHOD__ . ': not supported' );
2119 }
2120
2121 public function splitExt() {
2122 throw new MWException( __METHOD__ . ': not supported' );
2123 }
2124
2125 public function splitHeading() {
2126 throw new MWException( __METHOD__ . ': not supported' );
2127 }
2128}
2129
2133// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
2134class PPNode_Hash_Array implements PPNode {
2135
2136 public $value;
2137
2138 public function __construct( $value ) {
2139 $this->value = $value;
2140 }
2141
2142 public function __toString() {
2143 return var_export( $this, true );
2144 }
2145
2146 public function getLength() {
2147 return count( $this->value );
2148 }
2149
2150 public function item( $i ) {
2151 return $this->value[$i];
2152 }
2153
2154 public function getName() {
2155 return '#nodelist';
2156 }
2157
2158 public function getNextSibling() {
2159 return false;
2160 }
2161
2162 public function getChildren() {
2163 return false;
2164 }
2165
2166 public function getFirstChild() {
2167 return false;
2168 }
2169
2170 public function getChildrenOfType( $name ) {
2171 return false;
2172 }
2173
2174 public function splitArg() {
2175 throw new MWException( __METHOD__ . ': not supported' );
2176 }
2177
2178 public function splitExt() {
2179 throw new MWException( __METHOD__ . ': not supported' );
2180 }
2181
2182 public function splitHeading() {
2183 throw new MWException( __METHOD__ . ': not supported' );
2184 }
2185}
2186
2190// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
2191class PPNode_Hash_Attr implements PPNode {
2192
2193 public $name, $value;
2194 private $store, $index;
2195
2203 public function __construct( array $store, $index ) {
2204 $descriptor = $store[$index];
2205 if ( $descriptor[PPNode_Hash_Tree::NAME][0] !== '@' ) {
2206 throw new MWException( __METHOD__ . ': invalid name in attribute descriptor' );
2207 }
2208 $this->name = substr( $descriptor[PPNode_Hash_Tree::NAME], 1 );
2209 $this->value = $descriptor[PPNode_Hash_Tree::CHILDREN][0];
2210 $this->store = $store;
2211 $this->index = $index;
2212 }
2213
2214 public function __toString() {
2215 return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
2216 }
2217
2218 public function getName() {
2219 return $this->name;
2220 }
2221
2222 public function getNextSibling() {
2223 return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
2224 }
2225
2226 public function getChildren() {
2227 return false;
2228 }
2229
2230 public function getFirstChild() {
2231 return false;
2232 }
2233
2234 public function getChildrenOfType( $name ) {
2235 return false;
2236 }
2237
2238 public function getLength() {
2239 return false;
2240 }
2241
2242 public function item( $i ) {
2243 return false;
2244 }
2245
2246 public function splitArg() {
2247 throw new MWException( __METHOD__ . ': not supported' );
2248 }
2249
2250 public function splitExt() {
2251 throw new MWException( __METHOD__ . ': not supported' );
2252 }
2253
2254 public function splitHeading() {
2255 throw new MWException( __METHOD__ . ': not supported' );
2256 }
2257}
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:69
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:1423
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:271
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:2012
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 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:30