MediaWiki REL1_31
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 assert( $piece->open === "\n" );
508 $part = $piece->getCurrentPart();
509 // Search back through the input to see if it has a proper close.
510 // Do this using the reversed string since the other solutions
511 // (end anchor, etc.) are inefficient.
512 $wsLength = strspn( $revText, " \t", $lengthText - $i );
513 $searchStart = $i - $wsLength;
514 if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
515 // Comment found at line end
516 // Search for equals signs before the comment
517 $searchStart = $part->visualEnd;
518 $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
519 }
520 $count = $piece->count;
521 $equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
522 if ( $equalsLength > 0 ) {
523 if ( $searchStart - $equalsLength == $piece->startPos ) {
524 // This is just a single string of equals signs on its own line
525 // Replicate the doHeadings behavior /={count}(.+)={count}/
526 // First find out how many equals signs there really are (don't stop at 6)
527 $count = $equalsLength;
528 if ( $count < 3 ) {
529 $count = 0;
530 } else {
531 $count = min( 6, intval( ( $count - 1 ) / 2 ) );
532 }
533 } else {
534 $count = min( $equalsLength, $count );
535 }
536 if ( $count > 0 ) {
537 // Normal match, output <h>
538 $element = [ [ 'possible-h',
539 array_merge(
540 [
541 [ '@level', [ $count ] ],
542 [ '@i', [ $headingIndex++ ] ]
543 ],
544 $accum
545 )
546 ] ];
547 } else {
548 // Single equals sign on its own line, count=0
549 $element = $accum;
550 }
551 } else {
552 // No match, no <h>, just pass down the inner text
553 $element = $accum;
554 }
555 // Unwind the stack
556 $stack->pop();
557 $accum =& $stack->getAccum();
558 $stackFlags = $stack->getFlags();
559 if ( isset( $stackFlags['findEquals'] ) ) {
560 $findEquals = $stackFlags['findEquals'];
561 }
562 if ( isset( $stackFlags['findPipe'] ) ) {
563 $findPipe = $stackFlags['findPipe'];
564 }
565 if ( isset( $stackFlags['inHeading'] ) ) {
566 $inHeading = $stackFlags['inHeading'];
567 }
568
569 // Append the result to the enclosing accumulator
570 array_splice( $accum, count( $accum ), 0, $element );
571
572 // Note that we do NOT increment the input pointer.
573 // This is because the closing linebreak could be the opening linebreak of
574 // another heading. Infinite loops are avoided because the next iteration MUST
575 // hit the heading open case above, which unconditionally increments the
576 // input pointer.
577 } elseif ( $found == 'open' ) {
578 # count opening brace characters
579 $curLen = strlen( $curChar );
580 $count = ( $curLen > 1 ) ?
581 # allow the final character to repeat
582 strspn( $text, $curChar[$curLen - 1], $i + 1 ) + 1 :
583 strspn( $text, $curChar, $i );
584
585 $savedPrefix = '';
586 $lineStart = ( $i > 0 && $text[$i - 1] == "\n" );
587
588 if ( $curChar === "-{" && $count > $curLen ) {
589 // -{ => {{ transition because rightmost wins
590 $savedPrefix = '-';
591 $i++;
592 $curChar = '{';
593 $count--;
594 $rule = $this->rules[$curChar];
595 }
596
597 # we need to add to stack only if opening brace count is enough for one of the rules
598 if ( $count >= $rule['min'] ) {
599 # Add it to the stack
600 $piece = [
601 'open' => $curChar,
602 'close' => $rule['end'],
603 'savedPrefix' => $savedPrefix,
604 'count' => $count,
605 'lineStart' => $lineStart,
606 ];
607
608 $stack->push( $piece );
609 $accum =& $stack->getAccum();
610 $stackFlags = $stack->getFlags();
611 if ( isset( $stackFlags['findEquals'] ) ) {
612 $findEquals = $stackFlags['findEquals'];
613 }
614 if ( isset( $stackFlags['findPipe'] ) ) {
615 $findPipe = $stackFlags['findPipe'];
616 }
617 if ( isset( $stackFlags['inHeading'] ) ) {
618 $inHeading = $stackFlags['inHeading'];
619 }
620 } else {
621 # Add literal brace(s)
622 self::addLiteral( $accum, $savedPrefix . str_repeat( $curChar, $count ) );
623 }
624 $i += $count;
625 } elseif ( $found == 'close' ) {
626 $piece = $stack->top;
627 # lets check if there are enough characters for closing brace
628 $maxCount = $piece->count;
629 if ( $piece->close === '}-' && $curChar === '}' ) {
630 $maxCount--; # don't try to match closing '-' as a '}'
631 }
632 $curLen = strlen( $curChar );
633 $count = ( $curLen > 1 ) ? $curLen :
634 strspn( $text, $curChar, $i, $maxCount );
635
636 # check for maximum matching characters (if there are 5 closing
637 # characters, we will probably need only 3 - depending on the rules)
638 $rule = $this->rules[$piece->open];
639 if ( $count > $rule['max'] ) {
640 # The specified maximum exists in the callback array, unless the caller
641 # has made an error
642 $matchingCount = $rule['max'];
643 } else {
644 # Count is less than the maximum
645 # Skip any gaps in the callback array to find the true largest match
646 # Need to use array_key_exists not isset because the callback can be null
647 $matchingCount = $count;
648 while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
649 --$matchingCount;
650 }
651 }
652
653 if ( $matchingCount <= 0 ) {
654 # No matching element found in callback array
655 # Output a literal closing brace and continue
656 $endText = substr( $text, $i, $count );
657 self::addLiteral( $accum, $endText );
658 $i += $count;
659 continue;
660 }
661 $name = $rule['names'][$matchingCount];
662 if ( $name === null ) {
663 // No element, just literal text
664 $endText = substr( $text, $i, $matchingCount );
665 $element = $piece->breakSyntax( $matchingCount );
666 self::addLiteral( $element, $endText );
667 } else {
668 # Create XML element
669 $parts = $piece->parts;
670 $titleAccum = $parts[0]->out;
671 unset( $parts[0] );
672
673 $children = [];
674
675 # The invocation is at the start of the line if lineStart is set in
676 # the stack, and all opening brackets are used up.
677 if ( $maxCount == $matchingCount &&
678 !empty( $piece->lineStart ) &&
679 strlen( $piece->savedPrefix ) == 0 ) {
680 $children[] = [ '@lineStart', [ 1 ] ];
681 }
682 $titleNode = [ 'title', $titleAccum ];
683 $children[] = $titleNode;
684 $argIndex = 1;
685 foreach ( $parts as $part ) {
686 if ( isset( $part->eqpos ) ) {
687 $equalsNode = $part->out[$part->eqpos];
688 $nameNode = [ 'name', array_slice( $part->out, 0, $part->eqpos ) ];
689 $valueNode = [ 'value', array_slice( $part->out, $part->eqpos + 1 ) ];
690 $partNode = [ 'part', [ $nameNode, $equalsNode, $valueNode ] ];
691 $children[] = $partNode;
692 } else {
693 $nameNode = [ 'name', [ [ '@index', [ $argIndex++ ] ] ] ];
694 $valueNode = [ 'value', $part->out ];
695 $partNode = [ 'part', [ $nameNode, $valueNode ] ];
696 $children[] = $partNode;
697 }
698 }
699 $element = [ [ $name, $children ] ];
700 }
701
702 # Advance input pointer
703 $i += $matchingCount;
704
705 # Unwind the stack
706 $stack->pop();
707 $accum =& $stack->getAccum();
708
709 # Re-add the old stack element if it still has unmatched opening characters remaining
710 if ( $matchingCount < $piece->count ) {
711 $piece->parts = [ new PPDPart_Hash ];
712 $piece->count -= $matchingCount;
713 # do we still qualify for any callback with remaining count?
714 $min = $this->rules[$piece->open]['min'];
715 if ( $piece->count >= $min ) {
716 $stack->push( $piece );
717 $accum =& $stack->getAccum();
718 } elseif ( $piece->count == 1 && $piece->open === '{' && $piece->savedPrefix === '-' ) {
719 $piece->savedPrefix = '';
720 $piece->open = '-{';
721 $piece->count = 2;
722 $piece->close = $this->rules[$piece->open]['end'];
723 $stack->push( $piece );
724 $accum =& $stack->getAccum();
725 } else {
726 $s = substr( $piece->open, 0, -1 );
727 $s .= str_repeat(
728 substr( $piece->open, -1 ),
729 $piece->count - strlen( $s )
730 );
731 self::addLiteral( $accum, $piece->savedPrefix . $s );
732 }
733 } elseif ( $piece->savedPrefix !== '' ) {
734 self::addLiteral( $accum, $piece->savedPrefix );
735 }
736
737 $stackFlags = $stack->getFlags();
738 if ( isset( $stackFlags['findEquals'] ) ) {
739 $findEquals = $stackFlags['findEquals'];
740 }
741 if ( isset( $stackFlags['findPipe'] ) ) {
742 $findPipe = $stackFlags['findPipe'];
743 }
744 if ( isset( $stackFlags['inHeading'] ) ) {
745 $inHeading = $stackFlags['inHeading'];
746 }
747
748 # Add XML element to the enclosing accumulator
749 array_splice( $accum, count( $accum ), 0, $element );
750 } elseif ( $found == 'pipe' ) {
751 $findEquals = true; // shortcut for getFlags()
752 $stack->addPart();
753 $accum =& $stack->getAccum();
754 ++$i;
755 } elseif ( $found == 'equals' ) {
756 $findEquals = false; // shortcut for getFlags()
757 $accum[] = [ 'equals', [ '=' ] ];
758 $stack->getCurrentPart()->eqpos = count( $accum ) - 1;
759 ++$i;
760 }
761 }
762
763 # Output any remaining unclosed brackets
764 foreach ( $stack->stack as $piece ) {
765 array_splice( $stack->rootAccum, count( $stack->rootAccum ), 0, $piece->breakSyntax() );
766 }
767
768 # Enable top-level headings
769 foreach ( $stack->rootAccum as &$node ) {
770 if ( is_array( $node ) && $node[PPNode_Hash_Tree::NAME] === 'possible-h' ) {
771 $node[PPNode_Hash_Tree::NAME] = 'h';
772 }
773 }
774
775 $rootStore = [ [ 'root', $stack->rootAccum ] ];
776 $rootNode = new PPNode_Hash_Tree( $rootStore, 0 );
777
778 // Cache
779 $tree = json_encode( $rootStore, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
780 if ( $tree !== false ) {
781 $this->cacheSetTree( $text, $flags, $tree );
782 }
783
784 return $rootNode;
785 }
786
787 private static function addLiteral( array &$accum, $text ) {
788 $n = count( $accum );
789 if ( $n && is_string( $accum[$n - 1] ) ) {
790 $accum[$n - 1] .= $text;
791 } else {
792 $accum[] = $text;
793 }
794 }
795}
796
801// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
802class PPDStack_Hash extends PPDStack {
803
804 public function __construct() {
805 $this->elementClass = PPDStackElement_Hash::class;
806 parent::__construct();
807 $this->rootAccum = [];
808 }
809}
810
814// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
815class PPDStackElement_Hash extends PPDStackElement {
816
817 public function __construct( $data = [] ) {
818 $this->partClass = PPDPart_Hash::class;
819 parent::__construct( $data );
820 }
821
828 public function breakSyntax( $openingCount = false ) {
829 if ( $this->open == "\n" ) {
830 $accum = array_merge( [ $this->savedPrefix ], $this->parts[0]->out );
831 } else {
832 if ( $openingCount === false ) {
833 $openingCount = $this->count;
834 }
835 $s = substr( $this->open, 0, -1 );
836 $s .= str_repeat(
837 substr( $this->open, -1 ),
838 $openingCount - strlen( $s )
839 );
840 $accum = [ $this->savedPrefix . $s ];
841 $lastIndex = 0;
842 $first = true;
843 foreach ( $this->parts as $part ) {
844 if ( $first ) {
845 $first = false;
846 } elseif ( is_string( $accum[$lastIndex] ) ) {
847 $accum[$lastIndex] .= '|';
848 } else {
849 $accum[++$lastIndex] = '|';
850 }
851 foreach ( $part->out as $node ) {
852 if ( is_string( $node ) && is_string( $accum[$lastIndex] ) ) {
853 $accum[$lastIndex] .= $node;
854 } else {
855 $accum[++$lastIndex] = $node;
856 }
857 }
858 }
859 }
860 return $accum;
861 }
862}
863
867// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
868class PPDPart_Hash extends PPDPart {
869
870 public function __construct( $out = '' ) {
871 if ( $out !== '' ) {
872 $accum = [ $out ];
873 } else {
874 $accum = [];
875 }
876 parent::__construct( $accum );
877 }
878}
879
884// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
885class PPFrame_Hash implements PPFrame {
886
890 public $parser;
891
895 public $preprocessor;
896
900 public $title;
901 public $titleCache;
902
907 public $loopCheckHash;
908
913 public $depth;
914
915 private $volatile = false;
916 private $ttl = null;
917
921 protected $childExpansionCache;
922
927 public function __construct( $preprocessor ) {
928 $this->preprocessor = $preprocessor;
929 $this->parser = $preprocessor->parser;
930 $this->title = $this->parser->mTitle;
931 $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
932 $this->loopCheckHash = [];
933 $this->depth = 0;
934 $this->childExpansionCache = [];
935 }
936
947 public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
948 $namedArgs = [];
949 $numberedArgs = [];
950 if ( $title === false ) {
951 $title = $this->title;
952 }
953 if ( $args !== false ) {
954 if ( $args instanceof PPNode_Hash_Array ) {
955 $args = $args->value;
956 } elseif ( !is_array( $args ) ) {
957 throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
958 }
959 foreach ( $args as $arg ) {
960 $bits = $arg->splitArg();
961 if ( $bits['index'] !== '' ) {
962 // Numbered parameter
963 $index = $bits['index'] - $indexOffset;
964 if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
965 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
966 wfEscapeWikiText( $this->title ),
967 wfEscapeWikiText( $title ),
968 wfEscapeWikiText( $index ) )->text() );
969 $this->parser->addTrackingCategory( 'duplicate-args-category' );
970 }
971 $numberedArgs[$index] = $bits['value'];
972 unset( $namedArgs[$index] );
973 } else {
974 // Named parameter
975 $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
976 if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
977 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
978 wfEscapeWikiText( $this->title ),
979 wfEscapeWikiText( $title ),
980 wfEscapeWikiText( $name ) )->text() );
981 $this->parser->addTrackingCategory( 'duplicate-args-category' );
982 }
983 $namedArgs[$name] = $bits['value'];
984 unset( $numberedArgs[$name] );
985 }
986 }
987 }
988 return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
989 }
990
998 public function cachedExpand( $key, $root, $flags = 0 ) {
999 // we don't have a parent, so we don't have a cache
1000 return $this->expand( $root, $flags );
1001 }
1002
1009 public function expand( $root, $flags = 0 ) {
1010 static $expansionDepth = 0;
1011 if ( is_string( $root ) ) {
1012 return $root;
1013 }
1014
1015 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
1016 $this->parser->limitationWarn( 'node-count-exceeded',
1017 $this->parser->mPPNodeCount,
1018 $this->parser->mOptions->getMaxPPNodeCount()
1019 );
1020 return '<span class="error">Node-count limit exceeded</span>';
1021 }
1022 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
1023 $this->parser->limitationWarn( 'expansion-depth-exceeded',
1024 $expansionDepth,
1025 $this->parser->mOptions->getMaxPPExpandDepth()
1026 );
1027 return '<span class="error">Expansion depth limit exceeded</span>';
1028 }
1029 ++$expansionDepth;
1030 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
1031 $this->parser->mHighestExpansionDepth = $expansionDepth;
1032 }
1033
1034 $outStack = [ '', '' ];
1035 $iteratorStack = [ false, $root ];
1036 $indexStack = [ 0, 0 ];
1037
1038 while ( count( $iteratorStack ) > 1 ) {
1039 $level = count( $outStack ) - 1;
1040 $iteratorNode =& $iteratorStack[$level];
1041 $out =& $outStack[$level];
1042 $index =& $indexStack[$level];
1043
1044 if ( is_array( $iteratorNode ) ) {
1045 if ( $index >= count( $iteratorNode ) ) {
1046 // All done with this iterator
1047 $iteratorStack[$level] = false;
1048 $contextNode = false;
1049 } else {
1050 $contextNode = $iteratorNode[$index];
1051 $index++;
1052 }
1053 } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
1054 if ( $index >= $iteratorNode->getLength() ) {
1055 // All done with this iterator
1056 $iteratorStack[$level] = false;
1057 $contextNode = false;
1058 } else {
1059 $contextNode = $iteratorNode->item( $index );
1060 $index++;
1061 }
1062 } else {
1063 // Copy to $contextNode and then delete from iterator stack,
1064 // because this is not an iterator but we do have to execute it once
1065 $contextNode = $iteratorStack[$level];
1066 $iteratorStack[$level] = false;
1067 }
1068
1069 $newIterator = false;
1070 $contextName = false;
1071 $contextChildren = false;
1072
1073 if ( $contextNode === false ) {
1074 // nothing to do
1075 } elseif ( is_string( $contextNode ) ) {
1076 $out .= $contextNode;
1077 } elseif ( $contextNode instanceof PPNode_Hash_Array ) {
1078 $newIterator = $contextNode;
1079 } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
1080 // No output
1081 } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
1082 $out .= $contextNode->value;
1083 } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
1084 $contextName = $contextNode->name;
1085 $contextChildren = $contextNode->getRawChildren();
1086 } elseif ( is_array( $contextNode ) ) {
1087 // Node descriptor array
1088 if ( count( $contextNode ) !== 2 ) {
1089 throw new MWException( __METHOD__.
1090 ': found an array where a node descriptor should be' );
1091 }
1092 list( $contextName, $contextChildren ) = $contextNode;
1093 } else {
1094 throw new MWException( __METHOD__ . ': Invalid parameter type' );
1095 }
1096
1097 // Handle node descriptor array or tree object
1098 if ( $contextName === false ) {
1099 // Not a node, already handled above
1100 } elseif ( $contextName[0] === '@' ) {
1101 // Attribute: no output
1102 } elseif ( $contextName === 'template' ) {
1103 # Double-brace expansion
1104 $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
1105 if ( $flags & PPFrame::NO_TEMPLATES ) {
1106 $newIterator = $this->virtualBracketedImplode(
1107 '{{', '|', '}}',
1108 $bits['title'],
1109 $bits['parts']
1110 );
1111 } else {
1112 $ret = $this->parser->braceSubstitution( $bits, $this );
1113 if ( isset( $ret['object'] ) ) {
1114 $newIterator = $ret['object'];
1115 } else {
1116 $out .= $ret['text'];
1117 }
1118 }
1119 } elseif ( $contextName === 'tplarg' ) {
1120 # Triple-brace expansion
1121 $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
1122 if ( $flags & PPFrame::NO_ARGS ) {
1123 $newIterator = $this->virtualBracketedImplode(
1124 '{{{', '|', '}}}',
1125 $bits['title'],
1126 $bits['parts']
1127 );
1128 } else {
1129 $ret = $this->parser->argSubstitution( $bits, $this );
1130 if ( isset( $ret['object'] ) ) {
1131 $newIterator = $ret['object'];
1132 } else {
1133 $out .= $ret['text'];
1134 }
1135 }
1136 } elseif ( $contextName === 'comment' ) {
1137 # HTML-style comment
1138 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1139 # Not in RECOVER_COMMENTS mode (msgnw) though.
1140 if ( ( $this->parser->ot['html']
1141 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1142 || ( $flags & PPFrame::STRIP_COMMENTS )
1143 ) && !( $flags & PPFrame::RECOVER_COMMENTS )
1144 ) {
1145 $out .= '';
1146 } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1147 # Add a strip marker in PST mode so that pstPass2() can
1148 # run some old-fashioned regexes on the result.
1149 # Not in RECOVER_COMMENTS mode (extractSections) though.
1150 $out .= $this->parser->insertStripItem( $contextChildren[0] );
1151 } else {
1152 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1153 $out .= $contextChildren[0];
1154 }
1155 } elseif ( $contextName === 'ignore' ) {
1156 # Output suppression used by <includeonly> etc.
1157 # OT_WIKI will only respect <ignore> in substed templates.
1158 # The other output types respect it unless NO_IGNORE is set.
1159 # extractSections() sets NO_IGNORE and so never respects it.
1160 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1161 || ( $flags & PPFrame::NO_IGNORE )
1162 ) {
1163 $out .= $contextChildren[0];
1164 } else {
1165 // $out .= '';
1166 }
1167 } elseif ( $contextName === 'ext' ) {
1168 # Extension tag
1169 $bits = PPNode_Hash_Tree::splitRawExt( $contextChildren ) +
1170 [ 'attr' => null, 'inner' => null, 'close' => null ];
1171 if ( $flags & PPFrame::NO_TAGS ) {
1172 $s = '<' . $bits['name']->getFirstChild()->value;
1173 if ( $bits['attr'] ) {
1174 $s .= $bits['attr']->getFirstChild()->value;
1175 }
1176 if ( $bits['inner'] ) {
1177 $s .= '>' . $bits['inner']->getFirstChild()->value;
1178 if ( $bits['close'] ) {
1179 $s .= $bits['close']->getFirstChild()->value;
1180 }
1181 } else {
1182 $s .= '/>';
1183 }
1184 $out .= $s;
1185 } else {
1186 $out .= $this->parser->extensionSubstitution( $bits, $this );
1187 }
1188 } elseif ( $contextName === 'h' ) {
1189 # Heading
1190 if ( $this->parser->ot['html'] ) {
1191 # Expand immediately and insert heading index marker
1192 $s = $this->expand( $contextChildren, $flags );
1193 $bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
1194 $titleText = $this->title->getPrefixedDBkey();
1195 $this->parser->mHeadings[] = [ $titleText, $bits['i'] ];
1196 $serial = count( $this->parser->mHeadings ) - 1;
1197 $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
1198 $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
1199 $this->parser->mStripState->addGeneral( $marker, '' );
1200 $out .= $s;
1201 } else {
1202 # Expand in virtual stack
1203 $newIterator = $contextChildren;
1204 }
1205 } else {
1206 # Generic recursive expansion
1207 $newIterator = $contextChildren;
1208 }
1209
1210 if ( $newIterator !== false ) {
1211 $outStack[] = '';
1212 $iteratorStack[] = $newIterator;
1213 $indexStack[] = 0;
1214 } elseif ( $iteratorStack[$level] === false ) {
1215 // Return accumulated value to parent
1216 // With tail recursion
1217 while ( $iteratorStack[$level] === false && $level > 0 ) {
1218 $outStack[$level - 1] .= $out;
1219 array_pop( $outStack );
1220 array_pop( $iteratorStack );
1221 array_pop( $indexStack );
1222 $level--;
1223 }
1224 }
1225 }
1226 --$expansionDepth;
1227 return $outStack[0];
1228 }
1229
1236 public function implodeWithFlags( $sep, $flags /*, ... */ ) {
1237 $args = array_slice( func_get_args(), 2 );
1238
1239 $first = true;
1240 $s = '';
1241 foreach ( $args as $root ) {
1242 if ( $root instanceof PPNode_Hash_Array ) {
1243 $root = $root->value;
1244 }
1245 if ( !is_array( $root ) ) {
1246 $root = [ $root ];
1247 }
1248 foreach ( $root as $node ) {
1249 if ( $first ) {
1250 $first = false;
1251 } else {
1252 $s .= $sep;
1253 }
1254 $s .= $this->expand( $node, $flags );
1255 }
1256 }
1257 return $s;
1258 }
1259
1267 public function implode( $sep /*, ... */ ) {
1268 $args = array_slice( func_get_args(), 1 );
1269
1270 $first = true;
1271 $s = '';
1272 foreach ( $args as $root ) {
1273 if ( $root instanceof PPNode_Hash_Array ) {
1274 $root = $root->value;
1275 }
1276 if ( !is_array( $root ) ) {
1277 $root = [ $root ];
1278 }
1279 foreach ( $root as $node ) {
1280 if ( $first ) {
1281 $first = false;
1282 } else {
1283 $s .= $sep;
1284 }
1285 $s .= $this->expand( $node );
1286 }
1287 }
1288 return $s;
1289 }
1290
1299 public function virtualImplode( $sep /*, ... */ ) {
1300 $args = array_slice( func_get_args(), 1 );
1301 $out = [];
1302 $first = true;
1303
1304 foreach ( $args as $root ) {
1305 if ( $root instanceof PPNode_Hash_Array ) {
1306 $root = $root->value;
1307 }
1308 if ( !is_array( $root ) ) {
1309 $root = [ $root ];
1310 }
1311 foreach ( $root as $node ) {
1312 if ( $first ) {
1313 $first = false;
1314 } else {
1315 $out[] = $sep;
1316 }
1317 $out[] = $node;
1318 }
1319 }
1320 return new PPNode_Hash_Array( $out );
1321 }
1322
1332 public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1333 $args = array_slice( func_get_args(), 3 );
1334 $out = [ $start ];
1335 $first = true;
1336
1337 foreach ( $args as $root ) {
1338 if ( $root instanceof PPNode_Hash_Array ) {
1339 $root = $root->value;
1340 }
1341 if ( !is_array( $root ) ) {
1342 $root = [ $root ];
1343 }
1344 foreach ( $root as $node ) {
1345 if ( $first ) {
1346 $first = false;
1347 } else {
1348 $out[] = $sep;
1349 }
1350 $out[] = $node;
1351 }
1352 }
1353 $out[] = $end;
1354 return new PPNode_Hash_Array( $out );
1355 }
1356
1357 public function __toString() {
1358 return 'frame{}';
1359 }
1360
1365 public function getPDBK( $level = false ) {
1366 if ( $level === false ) {
1367 return $this->title->getPrefixedDBkey();
1368 } else {
1369 return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
1370 }
1371 }
1372
1376 public function getArguments() {
1377 return [];
1378 }
1379
1383 public function getNumberedArguments() {
1384 return [];
1385 }
1386
1390 public function getNamedArguments() {
1391 return [];
1392 }
1393
1399 public function isEmpty() {
1400 return true;
1401 }
1402
1407 public function getArgument( $name ) {
1408 return false;
1409 }
1410
1418 public function loopCheck( $title ) {
1419 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1420 }
1421
1427 public function isTemplate() {
1428 return false;
1429 }
1430
1436 public function getTitle() {
1437 return $this->title;
1438 }
1439
1445 public function setVolatile( $flag = true ) {
1446 $this->volatile = $flag;
1447 }
1448
1454 public function isVolatile() {
1455 return $this->volatile;
1456 }
1457
1463 public function setTTL( $ttl ) {
1464 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1465 $this->ttl = $ttl;
1466 }
1467 }
1468
1474 public function getTTL() {
1475 return $this->ttl;
1476 }
1477}
1478
1483// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1484class PPTemplateFrame_Hash extends PPFrame_Hash {
1485
1486 public $numberedArgs, $namedArgs, $parent;
1487 public $numberedExpansionCache, $namedExpansionCache;
1488
1496 public function __construct( $preprocessor, $parent = false, $numberedArgs = [],
1497 $namedArgs = [], $title = false
1498 ) {
1499 parent::__construct( $preprocessor );
1500
1501 $this->parent = $parent;
1502 $this->numberedArgs = $numberedArgs;
1503 $this->namedArgs = $namedArgs;
1504 $this->title = $title;
1505 $pdbk = $title ? $title->getPrefixedDBkey() : false;
1506 $this->titleCache = $parent->titleCache;
1507 $this->titleCache[] = $pdbk;
1508 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1509 if ( $pdbk !== false ) {
1510 $this->loopCheckHash[$pdbk] = true;
1511 }
1512 $this->depth = $parent->depth + 1;
1513 $this->numberedExpansionCache = $this->namedExpansionCache = [];
1514 }
1515
1516 public function __toString() {
1517 $s = 'tplframe{';
1518 $first = true;
1519 $args = $this->numberedArgs + $this->namedArgs;
1520 foreach ( $args as $name => $value ) {
1521 if ( $first ) {
1522 $first = false;
1523 } else {
1524 $s .= ', ';
1525 }
1526 $s .= "\"$name\":\"" .
1527 str_replace( '"', '\\"', $value->__toString() ) . '"';
1528 }
1529 $s .= '}';
1530 return $s;
1531 }
1532
1540 public function cachedExpand( $key, $root, $flags = 0 ) {
1541 if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1542 return $this->parent->childExpansionCache[$key];
1543 }
1544 $retval = $this->expand( $root, $flags );
1545 if ( !$this->isVolatile() ) {
1546 $this->parent->childExpansionCache[$key] = $retval;
1547 }
1548 return $retval;
1549 }
1550
1556 public function isEmpty() {
1557 return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1558 }
1559
1563 public function getArguments() {
1564 $arguments = [];
1565 foreach ( array_merge(
1566 array_keys( $this->numberedArgs ),
1567 array_keys( $this->namedArgs ) ) as $key ) {
1568 $arguments[$key] = $this->getArgument( $key );
1569 }
1570 return $arguments;
1571 }
1572
1576 public function getNumberedArguments() {
1577 $arguments = [];
1578 foreach ( array_keys( $this->numberedArgs ) as $key ) {
1579 $arguments[$key] = $this->getArgument( $key );
1580 }
1581 return $arguments;
1582 }
1583
1587 public function getNamedArguments() {
1588 $arguments = [];
1589 foreach ( array_keys( $this->namedArgs ) as $key ) {
1590 $arguments[$key] = $this->getArgument( $key );
1591 }
1592 return $arguments;
1593 }
1594
1599 public function getNumberedArgument( $index ) {
1600 if ( !isset( $this->numberedArgs[$index] ) ) {
1601 return false;
1602 }
1603 if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1604 # No trimming for unnamed arguments
1605 $this->numberedExpansionCache[$index] = $this->parent->expand(
1606 $this->numberedArgs[$index],
1607 PPFrame::STRIP_COMMENTS
1608 );
1609 }
1610 return $this->numberedExpansionCache[$index];
1611 }
1612
1617 public function getNamedArgument( $name ) {
1618 if ( !isset( $this->namedArgs[$name] ) ) {
1619 return false;
1620 }
1621 if ( !isset( $this->namedExpansionCache[$name] ) ) {
1622 # Trim named arguments post-expand, for backwards compatibility
1623 $this->namedExpansionCache[$name] = trim(
1624 $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1625 }
1626 return $this->namedExpansionCache[$name];
1627 }
1628
1633 public function getArgument( $name ) {
1634 $text = $this->getNumberedArgument( $name );
1635 if ( $text === false ) {
1636 $text = $this->getNamedArgument( $name );
1637 }
1638 return $text;
1639 }
1640
1646 public function isTemplate() {
1647 return true;
1648 }
1649
1650 public function setVolatile( $flag = true ) {
1651 parent::setVolatile( $flag );
1652 $this->parent->setVolatile( $flag );
1653 }
1654
1655 public function setTTL( $ttl ) {
1656 parent::setTTL( $ttl );
1657 $this->parent->setTTL( $ttl );
1658 }
1659}
1660
1665// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1666class PPCustomFrame_Hash extends PPFrame_Hash {
1667
1668 public $args;
1669
1670 public function __construct( $preprocessor, $args ) {
1671 parent::__construct( $preprocessor );
1672 $this->args = $args;
1673 }
1674
1675 public function __toString() {
1676 $s = 'cstmframe{';
1677 $first = true;
1678 foreach ( $this->args as $name => $value ) {
1679 if ( $first ) {
1680 $first = false;
1681 } else {
1682 $s .= ', ';
1683 }
1684 $s .= "\"$name\":\"" .
1685 str_replace( '"', '\\"', $value->__toString() ) . '"';
1686 }
1687 $s .= '}';
1688 return $s;
1689 }
1690
1694 public function isEmpty() {
1695 return !count( $this->args );
1696 }
1697
1702 public function getArgument( $index ) {
1703 if ( !isset( $this->args[$index] ) ) {
1704 return false;
1705 }
1706 return $this->args[$index];
1707 }
1708
1709 public function getArguments() {
1710 return $this->args;
1711 }
1712}
1713
1717// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1718class PPNode_Hash_Tree implements PPNode {
1719
1720 public $name;
1721
1728
1732 private $store;
1733
1737 private $index;
1738
1743 const NAME = 0;
1744
1749 const CHILDREN = 1;
1750
1758 public function __construct( array $store, $index ) {
1759 $this->store = $store;
1760 $this->index = $index;
1761 list( $this->name, $this->rawChildren ) = $this->store[$index];
1762 }
1763
1772 public static function factory( array $store, $index ) {
1773 if ( !isset( $store[$index] ) ) {
1774 return false;
1775 }
1776
1777 $descriptor = $store[$index];
1778 if ( is_string( $descriptor ) ) {
1779 $class = PPNode_Hash_Text::class;
1780 } elseif ( is_array( $descriptor ) ) {
1781 if ( $descriptor[self::NAME][0] === '@' ) {
1782 $class = PPNode_Hash_Attr::class;
1783 } else {
1784 $class = self::class;
1785 }
1786 } else {
1787 throw new MWException( __METHOD__.': invalid node descriptor' );
1788 }
1789 return new $class( $store, $index );
1790 }
1791
1795 public function __toString() {
1796 $inner = '';
1797 $attribs = '';
1798 for ( $node = $this->getFirstChild(); $node; $node = $node->getNextSibling() ) {
1799 if ( $node instanceof PPNode_Hash_Attr ) {
1800 $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
1801 } else {
1802 $inner .= $node->__toString();
1803 }
1804 }
1805 if ( $inner === '' ) {
1806 return "<{$this->name}$attribs/>";
1807 } else {
1808 return "<{$this->name}$attribs>$inner</{$this->name}>";
1809 }
1810 }
1811
1815 public function getChildren() {
1816 $children = [];
1817 foreach ( $this->rawChildren as $i => $child ) {
1818 $children[] = self::factory( $this->rawChildren, $i );
1819 }
1820 return new PPNode_Hash_Array( $children );
1821 }
1822
1830 public function getFirstChild() {
1831 if ( !isset( $this->rawChildren[0] ) ) {
1832 return false;
1833 } else {
1834 return self::factory( $this->rawChildren, 0 );
1835 }
1836 }
1837
1845 public function getNextSibling() {
1846 return self::factory( $this->store, $this->index + 1 );
1847 }
1848
1855 public function getChildrenOfType( $name ) {
1856 $children = [];
1857 foreach ( $this->rawChildren as $i => $child ) {
1858 if ( is_array( $child ) && $child[self::NAME] === $name ) {
1859 $children[] = self::factory( $this->rawChildren, $i );
1860 }
1861 }
1862 return new PPNode_Hash_Array( $children );
1863 }
1864
1869 public function getRawChildren() {
1870 return $this->rawChildren;
1871 }
1872
1876 public function getLength() {
1877 return false;
1878 }
1879
1884 public function item( $i ) {
1885 return false;
1886 }
1887
1891 public function getName() {
1892 return $this->name;
1893 }
1894
1904 public function splitArg() {
1905 return self::splitRawArg( $this->rawChildren );
1906 }
1907
1913 public static function splitRawArg( array $children ) {
1914 $bits = [];
1915 foreach ( $children as $i => $child ) {
1916 if ( !is_array( $child ) ) {
1917 continue;
1918 }
1919 if ( $child[self::NAME] === 'name' ) {
1920 $bits['name'] = new self( $children, $i );
1921 if ( isset( $child[self::CHILDREN][0][self::NAME] )
1922 && $child[self::CHILDREN][0][self::NAME] === '@index'
1923 ) {
1924 $bits['index'] = $child[self::CHILDREN][0][self::CHILDREN][0];
1925 }
1926 } elseif ( $child[self::NAME] === 'value' ) {
1927 $bits['value'] = new self( $children, $i );
1928 }
1929 }
1930
1931 if ( !isset( $bits['name'] ) ) {
1932 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
1933 }
1934 if ( !isset( $bits['index'] ) ) {
1935 $bits['index'] = '';
1936 }
1937 return $bits;
1938 }
1939
1947 public function splitExt() {
1948 return self::splitRawExt( $this->rawChildren );
1949 }
1950
1956 public static function splitRawExt( array $children ) {
1957 $bits = [];
1958 foreach ( $children as $i => $child ) {
1959 if ( !is_array( $child ) ) {
1960 continue;
1961 }
1962 switch ( $child[self::NAME] ) {
1963 case 'name':
1964 $bits['name'] = new self( $children, $i );
1965 break;
1966 case 'attr':
1967 $bits['attr'] = new self( $children, $i );
1968 break;
1969 case 'inner':
1970 $bits['inner'] = new self( $children, $i );
1971 break;
1972 case 'close':
1973 $bits['close'] = new self( $children, $i );
1974 break;
1975 }
1976 }
1977 if ( !isset( $bits['name'] ) ) {
1978 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
1979 }
1980 return $bits;
1981 }
1982
1989 public function splitHeading() {
1990 if ( $this->name !== 'h' ) {
1991 throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1992 }
1993 return self::splitRawHeading( $this->rawChildren );
1994 }
1995
2001 public static function splitRawHeading( array $children ) {
2002 $bits = [];
2003 foreach ( $children as $i => $child ) {
2004 if ( !is_array( $child ) ) {
2005 continue;
2006 }
2007 if ( $child[self::NAME] === '@i' ) {
2008 $bits['i'] = $child[self::CHILDREN][0];
2009 } elseif ( $child[self::NAME] === '@level' ) {
2010 $bits['level'] = $child[self::CHILDREN][0];
2011 }
2012 }
2013 if ( !isset( $bits['i'] ) ) {
2014 throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
2015 }
2016 return $bits;
2017 }
2018
2025 public function splitTemplate() {
2026 return self::splitRawTemplate( $this->rawChildren );
2027 }
2028
2034 public static function splitRawTemplate( array $children ) {
2035 $parts = [];
2036 $bits = [ 'lineStart' => '' ];
2037 foreach ( $children as $i => $child ) {
2038 if ( !is_array( $child ) ) {
2039 continue;
2040 }
2041 switch ( $child[self::NAME] ) {
2042 case 'title':
2043 $bits['title'] = new self( $children, $i );
2044 break;
2045 case 'part':
2046 $parts[] = new self( $children, $i );
2047 break;
2048 case '@lineStart':
2049 $bits['lineStart'] = '1';
2050 break;
2051 }
2052 }
2053 if ( !isset( $bits['title'] ) ) {
2054 throw new MWException( 'Invalid node passed to ' . __METHOD__ );
2055 }
2056 $bits['parts'] = new PPNode_Hash_Array( $parts );
2057 return $bits;
2058 }
2059}
2060
2064// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
2065class PPNode_Hash_Text implements PPNode {
2066
2067 public $value;
2068 private $store, $index;
2069
2077 public function __construct( array $store, $index ) {
2078 $this->value = $store[$index];
2079 if ( !is_scalar( $this->value ) ) {
2080 throw new MWException( __CLASS__ . ' given object instead of string' );
2081 }
2082 $this->store = $store;
2083 $this->index = $index;
2084 }
2085
2086 public function __toString() {
2087 return htmlspecialchars( $this->value );
2088 }
2089
2090 public function getNextSibling() {
2091 return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
2092 }
2093
2094 public function getChildren() {
2095 return false;
2096 }
2097
2098 public function getFirstChild() {
2099 return false;
2100 }
2101
2102 public function getChildrenOfType( $name ) {
2103 return false;
2104 }
2105
2106 public function getLength() {
2107 return false;
2108 }
2109
2110 public function item( $i ) {
2111 return false;
2112 }
2113
2114 public function getName() {
2115 return '#text';
2116 }
2117
2118 public function splitArg() {
2119 throw new MWException( __METHOD__ . ': not supported' );
2120 }
2121
2122 public function splitExt() {
2123 throw new MWException( __METHOD__ . ': not supported' );
2124 }
2125
2126 public function splitHeading() {
2127 throw new MWException( __METHOD__ . ': not supported' );
2128 }
2129}
2130
2134// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
2135class PPNode_Hash_Array implements PPNode {
2136
2137 public $value;
2138
2139 public function __construct( $value ) {
2140 $this->value = $value;
2141 }
2142
2143 public function __toString() {
2144 return var_export( $this, true );
2145 }
2146
2147 public function getLength() {
2148 return count( $this->value );
2149 }
2150
2151 public function item( $i ) {
2152 return $this->value[$i];
2153 }
2154
2155 public function getName() {
2156 return '#nodelist';
2157 }
2158
2159 public function getNextSibling() {
2160 return false;
2161 }
2162
2163 public function getChildren() {
2164 return false;
2165 }
2166
2167 public function getFirstChild() {
2168 return false;
2169 }
2170
2171 public function getChildrenOfType( $name ) {
2172 return false;
2173 }
2174
2175 public function splitArg() {
2176 throw new MWException( __METHOD__ . ': not supported' );
2177 }
2178
2179 public function splitExt() {
2180 throw new MWException( __METHOD__ . ': not supported' );
2181 }
2182
2183 public function splitHeading() {
2184 throw new MWException( __METHOD__ . ': not supported' );
2185 }
2186}
2187
2191// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
2192class PPNode_Hash_Attr implements PPNode {
2193
2194 public $name, $value;
2195 private $store, $index;
2196
2204 public function __construct( array $store, $index ) {
2205 $descriptor = $store[$index];
2206 if ( $descriptor[PPNode_Hash_Tree::NAME][0] !== '@' ) {
2207 throw new MWException( __METHOD__.': invalid name in attribute descriptor' );
2208 }
2209 $this->name = substr( $descriptor[PPNode_Hash_Tree::NAME], 1 );
2210 $this->value = $descriptor[PPNode_Hash_Tree::CHILDREN][0];
2211 $this->store = $store;
2212 $this->index = $index;
2213 }
2214
2215 public function __toString() {
2216 return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
2217 }
2218
2219 public function getName() {
2220 return $this->name;
2221 }
2222
2223 public function getNextSibling() {
2224 return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
2225 }
2226
2227 public function getChildren() {
2228 return false;
2229 }
2230
2231 public function getFirstChild() {
2232 return false;
2233 }
2234
2235 public function getChildrenOfType( $name ) {
2236 return false;
2237 }
2238
2239 public function getLength() {
2240 return false;
2241 }
2242
2243 public function item( $i ) {
2244 return false;
2245 }
2246
2247 public function splitArg() {
2248 throw new MWException( __METHOD__ . ': not supported' );
2249 }
2250
2251 public function splitExt() {
2252 throw new MWException( __METHOD__ . ': not supported' );
2253 }
2254
2255 public function splitHeading() {
2256 throw new MWException( __METHOD__ . ': not supported' );
2257 }
2258}
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
$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:70
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
when a variable name is used in a it is silently declared as a new local masking the global
Definition design.txt:95
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition design.txt:18
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at name
Definition design.txt:12
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
the array() calling protocol came about after MediaWiki 1.4rc1.
in this case you re responsible for computing and outputting the entire conflict part
Definition hooks.txt:1421
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:2014
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
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
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