MediaWiki REL1_30
Preprocessor_DOM.php
Go to the documentation of this file.
1<?php
27// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
29 // @codingStandardsIgnoreEnd
30
34 public $parser;
35
37
38 const CACHE_PREFIX = 'preprocess-xml';
39
40 public function __construct( $parser ) {
41 $this->parser = $parser;
42 $mem = ini_get( 'memory_limit' );
43 $this->memoryLimit = false;
44 if ( strval( $mem ) !== '' && $mem != -1 ) {
45 if ( preg_match( '/^\d+$/', $mem ) ) {
46 $this->memoryLimit = $mem;
47 } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) {
48 $this->memoryLimit = $m[1] * 1048576;
49 }
50 }
51 }
52
56 public function newFrame() {
57 return new PPFrame_DOM( $this );
58 }
59
64 public function newCustomFrame( $args ) {
65 return new PPCustomFrame_DOM( $this, $args );
66 }
67
73 public function newPartNodeArray( $values ) {
74 // NOTE: DOM manipulation is slower than building & parsing XML! (or so Tim sais)
75 $xml = "<list>";
76
77 foreach ( $values as $k => $val ) {
78 if ( is_int( $k ) ) {
79 $xml .= "<part><name index=\"$k\"/><value>"
80 . htmlspecialchars( $val ) . "</value></part>";
81 } else {
82 $xml .= "<part><name>" . htmlspecialchars( $k )
83 . "</name>=<value>" . htmlspecialchars( $val ) . "</value></part>";
84 }
85 }
86
87 $xml .= "</list>";
88
89 $dom = new DOMDocument();
90 MediaWiki\suppressWarnings();
91 $result = $dom->loadXML( $xml );
92 MediaWiki\restoreWarnings();
93 if ( !$result ) {
94 // Try running the XML through UtfNormal to get rid of invalid characters
95 $xml = UtfNormal\Validator::cleanUp( $xml );
96 // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2
97 // don't barf when the XML is >256 levels deep
98 $result = $dom->loadXML( $xml, 1 << 19 );
99 }
100
101 if ( !$result ) {
102 throw new MWException( 'Parameters passed to ' . __METHOD__ . ' result in invalid XML' );
103 }
104
105 $root = $dom->documentElement;
106 $node = new PPNode_DOM( $root->childNodes );
107 return $node;
108 }
109
114 public function memCheck() {
115 if ( $this->memoryLimit === false ) {
116 return true;
117 }
118 $usage = memory_get_usage();
119 if ( $usage > $this->memoryLimit * 0.9 ) {
120 $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 );
121 throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" );
122 }
123 return $usage <= $this->memoryLimit * 0.8;
124 }
125
150 public function preprocessToObj( $text, $flags = 0 ) {
151 $xml = $this->cacheGetTree( $text, $flags );
152 if ( $xml === false ) {
153 $xml = $this->preprocessToXml( $text, $flags );
154 $this->cacheSetTree( $text, $flags, $xml );
155 }
156
157 // Fail if the number of elements exceeds acceptable limits
158 // Do not attempt to generate the DOM
159 $this->parser->mGeneratedPPNodeCount += substr_count( $xml, '<' );
160 $max = $this->parser->mOptions->getMaxGeneratedPPNodeCount();
161 if ( $this->parser->mGeneratedPPNodeCount > $max ) {
162 // if ( $cacheable ) { ... }
163 throw new MWException( __METHOD__ . ': generated node count limit exceeded' );
164 }
165
166 $dom = new DOMDocument;
167 MediaWiki\suppressWarnings();
168 $result = $dom->loadXML( $xml );
169 MediaWiki\restoreWarnings();
170 if ( !$result ) {
171 // Try running the XML through UtfNormal to get rid of invalid characters
172 $xml = UtfNormal\Validator::cleanUp( $xml );
173 // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2
174 // don't barf when the XML is >256 levels deep.
175 $result = $dom->loadXML( $xml, 1 << 19 );
176 }
177 if ( $result ) {
178 $obj = new PPNode_DOM( $dom->documentElement );
179 }
180
181 // if ( $cacheable ) { ... }
182
183 if ( !$result ) {
184 throw new MWException( __METHOD__ . ' generated invalid XML' );
185 }
186 return $obj;
187 }
188
194 public function preprocessToXml( $text, $flags = 0 ) {
196
197 $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
198
199 $xmlishElements = $this->parser->getStripList();
200 $xmlishAllowMissingEndTag = [ 'includeonly', 'noinclude', 'onlyinclude' ];
201 $enableOnlyinclude = false;
202 if ( $forInclusion ) {
203 $ignoredTags = [ 'includeonly', '/includeonly' ];
204 $ignoredElements = [ 'noinclude' ];
205 $xmlishElements[] = 'noinclude';
206 if ( strpos( $text, '<onlyinclude>' ) !== false
207 && strpos( $text, '</onlyinclude>' ) !== false
208 ) {
209 $enableOnlyinclude = true;
210 }
211 } else {
212 $ignoredTags = [ 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ];
213 $ignoredElements = [ 'includeonly' ];
214 $xmlishElements[] = 'includeonly';
215 }
216 $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
217
218 // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
219 $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
220
221 $stack = new PPDStack;
222
223 $searchBase = "[{<\n"; # }
225 $searchBase .= '-';
226 }
227
228 // For fast reverse searches
229 $revText = strrev( $text );
230 $lengthText = strlen( $text );
231
232 // Input pointer, starts out pointing to a pseudo-newline before the start
233 $i = 0;
234 // Current accumulator
235 $accum =& $stack->getAccum();
236 $accum = '<root>';
237 // True to find equals signs in arguments
238 $findEquals = false;
239 // True to take notice of pipe characters
240 $findPipe = false;
241 $headingIndex = 1;
242 // True if $i is inside a possible heading
243 $inHeading = false;
244 // True if there are no more greater-than (>) signs right of $i
245 $noMoreGT = false;
246 // Map of tag name => true if there are no more closing tags of given type right of $i
247 $noMoreClosingTag = [];
248 // True to ignore all input up to the next <onlyinclude>
249 $findOnlyinclude = $enableOnlyinclude;
250 // Do a line-start run without outputting an LF character
251 $fakeLineStart = true;
252
253 while ( true ) {
254 // $this->memCheck();
255
256 if ( $findOnlyinclude ) {
257 // Ignore all input up to the next <onlyinclude>
258 $startPos = strpos( $text, '<onlyinclude>', $i );
259 if ( $startPos === false ) {
260 // Ignored section runs to the end
261 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>';
262 break;
263 }
264 $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
265 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>';
266 $i = $tagEndPos;
267 $findOnlyinclude = false;
268 }
269
270 if ( $fakeLineStart ) {
271 $found = 'line-start';
272 $curChar = '';
273 } else {
274 # Find next opening brace, closing brace or pipe
275 $search = $searchBase;
276 if ( $stack->top === false ) {
277 $currentClosing = '';
278 } elseif (
279 $stack->top->close === '}-' &&
280 $stack->top->count > 2
281 ) {
282 # adjust closing for -{{{...{{
283 $currentClosing = '}';
284 $search .= $currentClosing;
285 } else {
286 $currentClosing = $stack->top->close;
287 $search .= $currentClosing;
288 }
289 if ( $findPipe ) {
290 $search .= '|';
291 }
292 if ( $findEquals ) {
293 // First equals will be for the template
294 $search .= '=';
295 }
296 $rule = null;
297 # Output literal section, advance input counter
298 $literalLength = strcspn( $text, $search, $i );
299 if ( $literalLength > 0 ) {
300 $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) );
301 $i += $literalLength;
302 }
303 if ( $i >= $lengthText ) {
304 if ( $currentClosing == "\n" ) {
305 // Do a past-the-end run to finish off the heading
306 $curChar = '';
307 $found = 'line-end';
308 } else {
309 # All done
310 break;
311 }
312 } else {
313 $curChar = $curTwoChar = $text[$i];
314 if ( ( $i + 1 ) < $lengthText ) {
315 $curTwoChar .= $text[$i + 1];
316 }
317 if ( $curChar == '|' ) {
318 $found = 'pipe';
319 } elseif ( $curChar == '=' ) {
320 $found = 'equals';
321 } elseif ( $curChar == '<' ) {
322 $found = 'angle';
323 } elseif ( $curChar == "\n" ) {
324 if ( $inHeading ) {
325 $found = 'line-end';
326 } else {
327 $found = 'line-start';
328 }
329 } elseif ( $curTwoChar == $currentClosing ) {
330 $found = 'close';
331 $curChar = $curTwoChar;
332 } elseif ( $curChar == $currentClosing ) {
333 $found = 'close';
334 } elseif ( isset( $this->rules[$curTwoChar] ) ) {
335 $curChar = $curTwoChar;
336 $found = 'open';
337 $rule = $this->rules[$curChar];
338 } elseif ( isset( $this->rules[$curChar] ) ) {
339 $found = 'open';
340 $rule = $this->rules[$curChar];
341 } else {
342 # Some versions of PHP have a strcspn which stops on
343 # null characters; ignore these and continue.
344 # We also may get '-' and '}' characters here which
345 # don't match -{ or $currentClosing. Add these to
346 # output and continue.
347 if ( $curChar == '-' || $curChar == '}' ) {
348 $accum .= $curChar;
349 }
350 ++$i;
351 continue;
352 }
353 }
354 }
355
356 if ( $found == 'angle' ) {
357 $matches = false;
358 // Handle </onlyinclude>
359 if ( $enableOnlyinclude
360 && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>'
361 ) {
362 $findOnlyinclude = true;
363 continue;
364 }
365
366 // Determine element name
367 if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
368 // Element name missing or not listed
369 $accum .= '&lt;';
370 ++$i;
371 continue;
372 }
373 // Handle comments
374 if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
375 // To avoid leaving blank lines, when a sequence of
376 // space-separated comments is both preceded and followed by
377 // a newline (ignoring spaces), then
378 // trim leading and trailing spaces and the trailing newline.
379
380 // Find the end
381 $endPos = strpos( $text, '-->', $i + 4 );
382 if ( $endPos === false ) {
383 // Unclosed comment in input, runs to end
384 $inner = substr( $text, $i );
385 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
386 $i = $lengthText;
387 } else {
388 // Search backwards for leading whitespace
389 $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0;
390
391 // Search forwards for trailing whitespace
392 // $wsEnd will be the position of the last space (or the '>' if there's none)
393 $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 );
394
395 // Keep looking forward as long as we're finding more
396 // comments.
397 $comments = [ [ $wsStart, $wsEnd ] ];
398 while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) {
399 $c = strpos( $text, '-->', $wsEnd + 4 );
400 if ( $c === false ) {
401 break;
402 }
403 $c = $c + 2 + strspn( $text, " \t", $c + 3 );
404 $comments[] = [ $wsEnd + 1, $c ];
405 $wsEnd = $c;
406 }
407
408 // Eat the line if possible
409 // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
410 // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
411 // it's a possible beneficial b/c break.
412 if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
413 && substr( $text, $wsEnd + 1, 1 ) == "\n"
414 ) {
415 // Remove leading whitespace from the end of the accumulator
416 // Sanity check first though
417 $wsLength = $i - $wsStart;
418 if ( $wsLength > 0
419 && strspn( $accum, " \t", -$wsLength ) === $wsLength
420 ) {
421 $accum = substr( $accum, 0, -$wsLength );
422 }
423
424 // Dump all but the last comment to the accumulator
425 foreach ( $comments as $j => $com ) {
426 $startPos = $com[0];
427 $endPos = $com[1] + 1;
428 if ( $j == ( count( $comments ) - 1 ) ) {
429 break;
430 }
431 $inner = substr( $text, $startPos, $endPos - $startPos );
432 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
433 }
434
435 // Do a line-start run next time to look for headings after the comment
436 $fakeLineStart = true;
437 } else {
438 // No line to eat, just take the comment itself
439 $startPos = $i;
440 $endPos += 2;
441 }
442
443 if ( $stack->top ) {
444 $part = $stack->top->getCurrentPart();
445 if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) {
446 $part->visualEnd = $wsStart;
447 }
448 // Else comments abutting, no change in visual end
449 $part->commentEnd = $endPos;
450 }
451 $i = $endPos + 1;
452 $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
453 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
454 }
455 continue;
456 }
457 $name = $matches[1];
458 $lowerName = strtolower( $name );
459 $attrStart = $i + strlen( $name ) + 1;
460
461 // Find end of tag
462 $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
463 if ( $tagEndPos === false ) {
464 // Infinite backtrack
465 // Disable tag search to prevent worst-case O(N^2) performance
466 $noMoreGT = true;
467 $accum .= '&lt;';
468 ++$i;
469 continue;
470 }
471
472 // Handle ignored tags
473 if ( in_array( $lowerName, $ignoredTags ) ) {
474 $accum .= '<ignore>'
475 . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) )
476 . '</ignore>';
477 $i = $tagEndPos + 1;
478 continue;
479 }
480
481 $tagStartPos = $i;
482 if ( $text[$tagEndPos - 1] == '/' ) {
483 $attrEnd = $tagEndPos - 1;
484 $inner = null;
485 $i = $tagEndPos + 1;
486 $close = '';
487 } else {
488 $attrEnd = $tagEndPos;
489 // Find closing tag
490 if (
491 !isset( $noMoreClosingTag[$name] ) &&
492 preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
493 $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 )
494 ) {
495 $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
496 $i = $matches[0][1] + strlen( $matches[0][0] );
497 $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>';
498 } else {
499 // No end tag
500 if ( in_array( $name, $xmlishAllowMissingEndTag ) ) {
501 // Let it run out to the end of the text.
502 $inner = substr( $text, $tagEndPos + 1 );
503 $i = $lengthText;
504 $close = '';
505 } else {
506 // Don't match the tag, treat opening tag as literal and resume parsing.
507 $i = $tagEndPos + 1;
508 $accum .= htmlspecialchars( substr( $text, $tagStartPos, $tagEndPos + 1 - $tagStartPos ) );
509 // Cache results, otherwise we have O(N^2) performance for input like <foo><foo><foo>...
510 $noMoreClosingTag[$name] = true;
511 continue;
512 }
513 }
514 }
515 // <includeonly> and <noinclude> just become <ignore> tags
516 if ( in_array( $lowerName, $ignoredElements ) ) {
517 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) )
518 . '</ignore>';
519 continue;
520 }
521
522 $accum .= '<ext>';
523 if ( $attrEnd <= $attrStart ) {
524 $attr = '';
525 } else {
526 $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
527 }
528 $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' .
529 // Note that the attr element contains the whitespace between name and attribute,
530 // this is necessary for precise reconstruction during pre-save transform.
531 '<attr>' . htmlspecialchars( $attr ) . '</attr>';
532 if ( $inner !== null ) {
533 $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>';
534 }
535 $accum .= $close . '</ext>';
536 } elseif ( $found == 'line-start' ) {
537 // Is this the start of a heading?
538 // Line break belongs before the heading element in any case
539 if ( $fakeLineStart ) {
540 $fakeLineStart = false;
541 } else {
542 $accum .= $curChar;
543 $i++;
544 }
545
546 $count = strspn( $text, '=', $i, 6 );
547 if ( $count == 1 && $findEquals ) {
548 // DWIM: This looks kind of like a name/value separator.
549 // Let's let the equals handler have it and break the
550 // potential heading. This is heuristic, but AFAICT the
551 // methods for completely correct disambiguation are very
552 // complex.
553 } elseif ( $count > 0 ) {
554 $piece = [
555 'open' => "\n",
556 'close' => "\n",
557 'parts' => [ new PPDPart( str_repeat( '=', $count ) ) ],
558 'startPos' => $i,
559 'count' => $count ];
560 $stack->push( $piece );
561 $accum =& $stack->getAccum();
562 $flags = $stack->getFlags();
563 extract( $flags );
564 $i += $count;
565 }
566 } elseif ( $found == 'line-end' ) {
567 $piece = $stack->top;
568 // A heading must be open, otherwise \n wouldn't have been in the search list
569 assert( $piece->open === "\n" );
570 $part = $piece->getCurrentPart();
571 // Search back through the input to see if it has a proper close.
572 // Do this using the reversed string since the other solutions
573 // (end anchor, etc.) are inefficient.
574 $wsLength = strspn( $revText, " \t", $lengthText - $i );
575 $searchStart = $i - $wsLength;
576 if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
577 // Comment found at line end
578 // Search for equals signs before the comment
579 $searchStart = $part->visualEnd;
580 $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
581 }
582 $count = $piece->count;
583 $equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
584 if ( $equalsLength > 0 ) {
585 if ( $searchStart - $equalsLength == $piece->startPos ) {
586 // This is just a single string of equals signs on its own line
587 // Replicate the doHeadings behavior /={count}(.+)={count}/
588 // First find out how many equals signs there really are (don't stop at 6)
589 $count = $equalsLength;
590 if ( $count < 3 ) {
591 $count = 0;
592 } else {
593 $count = min( 6, intval( ( $count - 1 ) / 2 ) );
594 }
595 } else {
596 $count = min( $equalsLength, $count );
597 }
598 if ( $count > 0 ) {
599 // Normal match, output <h>
600 $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>";
601 $headingIndex++;
602 } else {
603 // Single equals sign on its own line, count=0
604 $element = $accum;
605 }
606 } else {
607 // No match, no <h>, just pass down the inner text
608 $element = $accum;
609 }
610 // Unwind the stack
611 $stack->pop();
612 $accum =& $stack->getAccum();
613 $flags = $stack->getFlags();
614 extract( $flags );
615
616 // Append the result to the enclosing accumulator
617 $accum .= $element;
618 // Note that we do NOT increment the input pointer.
619 // This is because the closing linebreak could be the opening linebreak of
620 // another heading. Infinite loops are avoided because the next iteration MUST
621 // hit the heading open case above, which unconditionally increments the
622 // input pointer.
623 } elseif ( $found == 'open' ) {
624 # count opening brace characters
625 $curLen = strlen( $curChar );
626 $count = ( $curLen > 1 ) ?
627 # allow the final character to repeat
628 strspn( $text, $curChar[$curLen - 1], $i + 1 ) + 1 :
629 strspn( $text, $curChar, $i );
630
631 # we need to add to stack only if opening brace count is enough for one of the rules
632 if ( $count >= $rule['min'] ) {
633 # Add it to the stack
634 $piece = [
635 'open' => $curChar,
636 'close' => $rule['end'],
637 'count' => $count,
638 'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ),
639 ];
640
641 $stack->push( $piece );
642 $accum =& $stack->getAccum();
643 $flags = $stack->getFlags();
644 extract( $flags );
645 } else {
646 # Add literal brace(s)
647 $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
648 }
649 $i += $count;
650 } elseif ( $found == 'close' ) {
651 $piece = $stack->top;
652 # lets check if there are enough characters for closing brace
653 $maxCount = $piece->count;
654 if ( $piece->close === '}-' && $curChar === '}' ) {
655 $maxCount--; # don't try to match closing '-' as a '}'
656 }
657 $curLen = strlen( $curChar );
658 $count = ( $curLen > 1 ) ? $curLen :
659 strspn( $text, $curChar, $i, $maxCount );
660
661 # check for maximum matching characters (if there are 5 closing
662 # characters, we will probably need only 3 - depending on the rules)
663 $rule = $this->rules[$piece->open];
664 if ( $piece->close === '}-' && $piece->count > 2 ) {
665 # tweak for -{..{{ }}..}-
666 $rule = $this->rules['{'];
667 }
668 if ( $count > $rule['max'] ) {
669 # The specified maximum exists in the callback array, unless the caller
670 # has made an error
671 $matchingCount = $rule['max'];
672 } else {
673 # Count is less than the maximum
674 # Skip any gaps in the callback array to find the true largest match
675 # Need to use array_key_exists not isset because the callback can be null
676 $matchingCount = $count;
677 while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
678 --$matchingCount;
679 }
680 }
681
682 if ( $matchingCount <= 0 ) {
683 # No matching element found in callback array
684 # Output a literal closing brace and continue
685 $endText = substr( $text, $i, $count );
686 $accum .= htmlspecialchars( $endText );
687 $i += $count;
688 continue;
689 }
690 $name = $rule['names'][$matchingCount];
691 if ( $name === null ) {
692 // No element, just literal text
693 $endText = substr( $text, $i, $matchingCount );
694 $element = $piece->breakSyntax( $matchingCount ) . $endText;
695 } else {
696 # Create XML element
697 # Note: $parts is already XML, does not need to be encoded further
698 $parts = $piece->parts;
699 $title = $parts[0]->out;
700 unset( $parts[0] );
701
702 # The invocation is at the start of the line if lineStart is set in
703 # the stack, and all opening brackets are used up.
704 if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
705 $attr = ' lineStart="1"';
706 } else {
707 $attr = '';
708 }
709
710 $element = "<$name$attr>";
711 $element .= "<title>$title</title>";
712 $argIndex = 1;
713 foreach ( $parts as $part ) {
714 if ( isset( $part->eqpos ) ) {
715 $argName = substr( $part->out, 0, $part->eqpos );
716 $argValue = substr( $part->out, $part->eqpos + 1 );
717 $element .= "<part><name>$argName</name>=<value>$argValue</value></part>";
718 } else {
719 $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>";
720 $argIndex++;
721 }
722 }
723 $element .= "</$name>";
724 }
725
726 # Advance input pointer
727 $i += $matchingCount;
728
729 # Unwind the stack
730 $stack->pop();
731 $accum =& $stack->getAccum();
732
733 # Re-add the old stack element if it still has unmatched opening characters remaining
734 if ( $matchingCount < $piece->count ) {
735 $piece->parts = [ new PPDPart ];
736 $piece->count -= $matchingCount;
737 # do we still qualify for any callback with remaining count?
738 $min = $this->rules[$piece->open]['min'];
739 if ( $piece->count >= $min ) {
740 $stack->push( $piece );
741 $accum =& $stack->getAccum();
742 } else {
743 $s = substr( $piece->open, 0, -1 );
744 $s .= str_repeat(
745 substr( $piece->open, -1 ),
746 $piece->count - strlen( $s )
747 );
748 $accum .= $s;
749 }
750 }
751 $flags = $stack->getFlags();
752 extract( $flags );
753
754 # Add XML element to the enclosing accumulator
755 $accum .= $element;
756 } elseif ( $found == 'pipe' ) {
757 $findEquals = true; // shortcut for getFlags()
758 $stack->addPart();
759 $accum =& $stack->getAccum();
760 ++$i;
761 } elseif ( $found == 'equals' ) {
762 $findEquals = false; // shortcut for getFlags()
763 $stack->getCurrentPart()->eqpos = strlen( $accum );
764 $accum .= '=';
765 ++$i;
766 } elseif ( $found == 'dash' ) {
767 $accum .= '-';
768 ++$i;
769 }
770 }
771
772 # Output any remaining unclosed brackets
773 foreach ( $stack->stack as $piece ) {
774 $stack->rootAccum .= $piece->breakSyntax();
775 }
776 $stack->rootAccum .= '</root>';
777 $xml = $stack->rootAccum;
778
779 return $xml;
780 }
781}
782
787class PPDStack {
788 public $stack, $rootAccum;
789
793 public $top;
794 public $out;
795 public $elementClass = 'PPDStackElement';
796
797 public static $false = false;
798
799 public function __construct() {
800 $this->stack = [];
801 $this->top = false;
802 $this->rootAccum = '';
803 $this->accum =& $this->rootAccum;
804 }
805
809 public function count() {
810 return count( $this->stack );
811 }
812
813 public function &getAccum() {
814 return $this->accum;
815 }
816
817 public function getCurrentPart() {
818 if ( $this->top === false ) {
819 return false;
820 } else {
821 return $this->top->getCurrentPart();
822 }
823 }
824
825 public function push( $data ) {
826 if ( $data instanceof $this->elementClass ) {
827 $this->stack[] = $data;
828 } else {
829 $class = $this->elementClass;
830 $this->stack[] = new $class( $data );
831 }
832 $this->top = $this->stack[count( $this->stack ) - 1];
833 $this->accum =& $this->top->getAccum();
834 }
835
836 public function pop() {
837 if ( !count( $this->stack ) ) {
838 throw new MWException( __METHOD__ . ': no elements remaining' );
839 }
840 $temp = array_pop( $this->stack );
841
842 if ( count( $this->stack ) ) {
843 $this->top = $this->stack[count( $this->stack ) - 1];
844 $this->accum =& $this->top->getAccum();
845 } else {
846 $this->top = self::$false;
847 $this->accum =& $this->rootAccum;
848 }
849 return $temp;
850 }
851
852 public function addPart( $s = '' ) {
853 $this->top->addPart( $s );
854 $this->accum =& $this->top->getAccum();
855 }
856
860 public function getFlags() {
861 if ( !count( $this->stack ) ) {
862 return [
863 'findEquals' => false,
864 'findPipe' => false,
865 'inHeading' => false,
866 ];
867 } else {
868 return $this->top->getFlags();
869 }
870 }
871}
872
876class PPDStackElement {
880 public $open;
881
885 public $close;
886
890 public $count;
891
895 public $parts;
896
901 public $lineStart;
902
903 public $partClass = 'PPDPart';
904
905 public function __construct( $data = [] ) {
906 $class = $this->partClass;
907 $this->parts = [ new $class ];
908
909 foreach ( $data as $name => $value ) {
910 $this->$name = $value;
911 }
912 }
913
914 public function &getAccum() {
915 return $this->parts[count( $this->parts ) - 1]->out;
916 }
917
918 public function addPart( $s = '' ) {
919 $class = $this->partClass;
920 $this->parts[] = new $class( $s );
921 }
922
923 public function getCurrentPart() {
924 return $this->parts[count( $this->parts ) - 1];
925 }
926
930 public function getFlags() {
931 $partCount = count( $this->parts );
932 $findPipe = $this->open != "\n" && $this->open != '[';
933 return [
934 'findPipe' => $findPipe,
935 'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ),
936 'inHeading' => $this->open == "\n",
937 ];
938 }
939
946 public function breakSyntax( $openingCount = false ) {
947 if ( $this->open == "\n" ) {
948 $s = $this->parts[0]->out;
949 } else {
950 if ( $openingCount === false ) {
951 $openingCount = $this->count;
952 }
953 $s = substr( $this->open, 0, -1 );
954 $s .= str_repeat(
955 substr( $this->open, -1 ),
956 $openingCount - strlen( $s )
957 );
958 $first = true;
959 foreach ( $this->parts as $part ) {
960 if ( $first ) {
961 $first = false;
962 } else {
963 $s .= '|';
964 }
965 $s .= $part->out;
966 }
967 }
968 return $s;
969 }
970}
971
975class PPDPart {
979 public $out;
980
981 // Optional member variables:
982 // eqpos Position of equals sign in output accumulator
983 // commentEnd Past-the-end input pointer for the last comment encountered
984 // visualEnd Past-the-end input pointer for the end of the accumulator minus comments
985
986 public function __construct( $out = '' ) {
987 $this->out = $out;
988 }
989}
990
995// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
996class PPFrame_DOM implements PPFrame {
997 // @codingStandardsIgnoreEnd
998
1002 public $preprocessor;
1003
1007 public $parser;
1008
1012 public $title;
1013 public $titleCache;
1014
1019 public $loopCheckHash;
1020
1025 public $depth;
1026
1027 private $volatile = false;
1028 private $ttl = null;
1029
1033 protected $childExpansionCache;
1034
1039 public function __construct( $preprocessor ) {
1040 $this->preprocessor = $preprocessor;
1041 $this->parser = $preprocessor->parser;
1042 $this->title = $this->parser->mTitle;
1043 $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
1044 $this->loopCheckHash = [];
1045 $this->depth = 0;
1046 $this->childExpansionCache = [];
1047 }
1048
1058 public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
1059 $namedArgs = [];
1060 $numberedArgs = [];
1061 if ( $title === false ) {
1062 $title = $this->title;
1063 }
1064 if ( $args !== false ) {
1065 $xpath = false;
1066 if ( $args instanceof PPNode ) {
1067 $args = $args->node;
1068 }
1069 foreach ( $args as $arg ) {
1070 if ( $arg instanceof PPNode ) {
1071 $arg = $arg->node;
1072 }
1073 if ( !$xpath || $xpath->document !== $arg->ownerDocument ) {
1074 $xpath = new DOMXPath( $arg->ownerDocument );
1075 }
1076
1077 $nameNodes = $xpath->query( 'name', $arg );
1078 $value = $xpath->query( 'value', $arg );
1079 if ( $nameNodes->item( 0 )->hasAttributes() ) {
1080 // Numbered parameter
1081 $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
1082 $index = $index - $indexOffset;
1083 if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
1084 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
1085 wfEscapeWikiText( $this->title ),
1086 wfEscapeWikiText( $title ),
1087 wfEscapeWikiText( $index ) )->text() );
1088 $this->parser->addTrackingCategory( 'duplicate-args-category' );
1089 }
1090 $numberedArgs[$index] = $value->item( 0 );
1091 unset( $namedArgs[$index] );
1092 } else {
1093 // Named parameter
1094 $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) );
1095 if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
1096 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
1097 wfEscapeWikiText( $this->title ),
1098 wfEscapeWikiText( $title ),
1099 wfEscapeWikiText( $name ) )->text() );
1100 $this->parser->addTrackingCategory( 'duplicate-args-category' );
1101 }
1102 $namedArgs[$name] = $value->item( 0 );
1103 unset( $numberedArgs[$name] );
1104 }
1105 }
1106 }
1107 return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
1108 }
1109
1117 public function cachedExpand( $key, $root, $flags = 0 ) {
1118 // we don't have a parent, so we don't have a cache
1119 return $this->expand( $root, $flags );
1120 }
1121
1128 public function expand( $root, $flags = 0 ) {
1129 static $expansionDepth = 0;
1130 if ( is_string( $root ) ) {
1131 return $root;
1132 }
1133
1134 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
1135 $this->parser->limitationWarn( 'node-count-exceeded',
1136 $this->parser->mPPNodeCount,
1137 $this->parser->mOptions->getMaxPPNodeCount()
1138 );
1139 return '<span class="error">Node-count limit exceeded</span>';
1140 }
1141
1142 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
1143 $this->parser->limitationWarn( 'expansion-depth-exceeded',
1144 $expansionDepth,
1145 $this->parser->mOptions->getMaxPPExpandDepth()
1146 );
1147 return '<span class="error">Expansion depth limit exceeded</span>';
1148 }
1149 ++$expansionDepth;
1150 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
1151 $this->parser->mHighestExpansionDepth = $expansionDepth;
1152 }
1153
1154 if ( $root instanceof PPNode_DOM ) {
1155 $root = $root->node;
1156 }
1157 if ( $root instanceof DOMDocument ) {
1158 $root = $root->documentElement;
1159 }
1160
1161 $outStack = [ '', '' ];
1162 $iteratorStack = [ false, $root ];
1163 $indexStack = [ 0, 0 ];
1164
1165 while ( count( $iteratorStack ) > 1 ) {
1166 $level = count( $outStack ) - 1;
1167 $iteratorNode =& $iteratorStack[$level];
1168 $out =& $outStack[$level];
1169 $index =& $indexStack[$level];
1170
1171 if ( $iteratorNode instanceof PPNode_DOM ) {
1172 $iteratorNode = $iteratorNode->node;
1173 }
1174
1175 if ( is_array( $iteratorNode ) ) {
1176 if ( $index >= count( $iteratorNode ) ) {
1177 // All done with this iterator
1178 $iteratorStack[$level] = false;
1179 $contextNode = false;
1180 } else {
1181 $contextNode = $iteratorNode[$index];
1182 $index++;
1183 }
1184 } elseif ( $iteratorNode instanceof DOMNodeList ) {
1185 if ( $index >= $iteratorNode->length ) {
1186 // All done with this iterator
1187 $iteratorStack[$level] = false;
1188 $contextNode = false;
1189 } else {
1190 $contextNode = $iteratorNode->item( $index );
1191 $index++;
1192 }
1193 } else {
1194 // Copy to $contextNode and then delete from iterator stack,
1195 // because this is not an iterator but we do have to execute it once
1196 $contextNode = $iteratorStack[$level];
1197 $iteratorStack[$level] = false;
1198 }
1199
1200 if ( $contextNode instanceof PPNode_DOM ) {
1201 $contextNode = $contextNode->node;
1202 }
1203
1204 $newIterator = false;
1205
1206 if ( $contextNode === false ) {
1207 // nothing to do
1208 } elseif ( is_string( $contextNode ) ) {
1209 $out .= $contextNode;
1210 } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) {
1211 $newIterator = $contextNode;
1212 } elseif ( $contextNode instanceof DOMNode ) {
1213 if ( $contextNode->nodeType == XML_TEXT_NODE ) {
1214 $out .= $contextNode->nodeValue;
1215 } elseif ( $contextNode->nodeName == 'template' ) {
1216 # Double-brace expansion
1217 $xpath = new DOMXPath( $contextNode->ownerDocument );
1218 $titles = $xpath->query( 'title', $contextNode );
1219 $title = $titles->item( 0 );
1220 $parts = $xpath->query( 'part', $contextNode );
1221 if ( $flags & PPFrame::NO_TEMPLATES ) {
1222 $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
1223 } else {
1224 $lineStart = $contextNode->getAttribute( 'lineStart' );
1225 $params = [
1226 'title' => new PPNode_DOM( $title ),
1227 'parts' => new PPNode_DOM( $parts ),
1228 'lineStart' => $lineStart ];
1229 $ret = $this->parser->braceSubstitution( $params, $this );
1230 if ( isset( $ret['object'] ) ) {
1231 $newIterator = $ret['object'];
1232 } else {
1233 $out .= $ret['text'];
1234 }
1235 }
1236 } elseif ( $contextNode->nodeName == 'tplarg' ) {
1237 # Triple-brace expansion
1238 $xpath = new DOMXPath( $contextNode->ownerDocument );
1239 $titles = $xpath->query( 'title', $contextNode );
1240 $title = $titles->item( 0 );
1241 $parts = $xpath->query( 'part', $contextNode );
1242 if ( $flags & PPFrame::NO_ARGS ) {
1243 $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
1244 } else {
1245 $params = [
1246 'title' => new PPNode_DOM( $title ),
1247 'parts' => new PPNode_DOM( $parts ) ];
1248 $ret = $this->parser->argSubstitution( $params, $this );
1249 if ( isset( $ret['object'] ) ) {
1250 $newIterator = $ret['object'];
1251 } else {
1252 $out .= $ret['text'];
1253 }
1254 }
1255 } elseif ( $contextNode->nodeName == 'comment' ) {
1256 # HTML-style comment
1257 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1258 # Not in RECOVER_COMMENTS mode (msgnw) though.
1259 if ( ( $this->parser->ot['html']
1260 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1261 || ( $flags & PPFrame::STRIP_COMMENTS )
1262 ) && !( $flags & PPFrame::RECOVER_COMMENTS )
1263 ) {
1264 $out .= '';
1265 } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1266 # Add a strip marker in PST mode so that pstPass2() can
1267 # run some old-fashioned regexes on the result.
1268 # Not in RECOVER_COMMENTS mode (extractSections) though.
1269 $out .= $this->parser->insertStripItem( $contextNode->textContent );
1270 } else {
1271 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1272 $out .= $contextNode->textContent;
1273 }
1274 } elseif ( $contextNode->nodeName == 'ignore' ) {
1275 # Output suppression used by <includeonly> etc.
1276 # OT_WIKI will only respect <ignore> in substed templates.
1277 # The other output types respect it unless NO_IGNORE is set.
1278 # extractSections() sets NO_IGNORE and so never respects it.
1279 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1280 || ( $flags & PPFrame::NO_IGNORE )
1281 ) {
1282 $out .= $contextNode->textContent;
1283 } else {
1284 $out .= '';
1285 }
1286 } elseif ( $contextNode->nodeName == 'ext' ) {
1287 # Extension tag
1288 $xpath = new DOMXPath( $contextNode->ownerDocument );
1289 $names = $xpath->query( 'name', $contextNode );
1290 $attrs = $xpath->query( 'attr', $contextNode );
1291 $inners = $xpath->query( 'inner', $contextNode );
1292 $closes = $xpath->query( 'close', $contextNode );
1293 if ( $flags & PPFrame::NO_TAGS ) {
1294 $s = '<' . $this->expand( $names->item( 0 ), $flags );
1295 if ( $attrs->length > 0 ) {
1296 $s .= $this->expand( $attrs->item( 0 ), $flags );
1297 }
1298 if ( $inners->length > 0 ) {
1299 $s .= '>' . $this->expand( $inners->item( 0 ), $flags );
1300 if ( $closes->length > 0 ) {
1301 $s .= $this->expand( $closes->item( 0 ), $flags );
1302 }
1303 } else {
1304 $s .= '/>';
1305 }
1306 $out .= $s;
1307 } else {
1308 $params = [
1309 'name' => new PPNode_DOM( $names->item( 0 ) ),
1310 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null,
1311 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null,
1312 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null,
1313 ];
1314 $out .= $this->parser->extensionSubstitution( $params, $this );
1315 }
1316 } elseif ( $contextNode->nodeName == 'h' ) {
1317 # Heading
1318 $s = $this->expand( $contextNode->childNodes, $flags );
1319
1320 # Insert a heading marker only for <h> children of <root>
1321 # This is to stop extractSections from going over multiple tree levels
1322 if ( $contextNode->parentNode->nodeName == 'root' && $this->parser->ot['html'] ) {
1323 # Insert heading index marker
1324 $headingIndex = $contextNode->getAttribute( 'i' );
1325 $titleText = $this->title->getPrefixedDBkey();
1326 $this->parser->mHeadings[] = [ $titleText, $headingIndex ];
1327 $serial = count( $this->parser->mHeadings ) - 1;
1328 $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
1329 $count = $contextNode->getAttribute( 'level' );
1330 $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
1331 $this->parser->mStripState->addGeneral( $marker, '' );
1332 }
1333 $out .= $s;
1334 } else {
1335 # Generic recursive expansion
1336 $newIterator = $contextNode->childNodes;
1337 }
1338 } else {
1339 throw new MWException( __METHOD__ . ': Invalid parameter type' );
1340 }
1341
1342 if ( $newIterator !== false ) {
1343 if ( $newIterator instanceof PPNode_DOM ) {
1344 $newIterator = $newIterator->node;
1345 }
1346 $outStack[] = '';
1347 $iteratorStack[] = $newIterator;
1348 $indexStack[] = 0;
1349 } elseif ( $iteratorStack[$level] === false ) {
1350 // Return accumulated value to parent
1351 // With tail recursion
1352 while ( $iteratorStack[$level] === false && $level > 0 ) {
1353 $outStack[$level - 1] .= $out;
1354 array_pop( $outStack );
1355 array_pop( $iteratorStack );
1356 array_pop( $indexStack );
1357 $level--;
1358 }
1359 }
1360 }
1361 --$expansionDepth;
1362 return $outStack[0];
1363 }
1364
1371 public function implodeWithFlags( $sep, $flags /*, ... */ ) {
1372 $args = array_slice( func_get_args(), 2 );
1373
1374 $first = true;
1375 $s = '';
1376 foreach ( $args as $root ) {
1377 if ( $root instanceof PPNode_DOM ) {
1378 $root = $root->node;
1379 }
1380 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1381 $root = [ $root ];
1382 }
1383 foreach ( $root as $node ) {
1384 if ( $first ) {
1385 $first = false;
1386 } else {
1387 $s .= $sep;
1388 }
1389 $s .= $this->expand( $node, $flags );
1390 }
1391 }
1392 return $s;
1393 }
1394
1403 public function implode( $sep /*, ... */ ) {
1404 $args = array_slice( func_get_args(), 1 );
1405
1406 $first = true;
1407 $s = '';
1408 foreach ( $args as $root ) {
1409 if ( $root instanceof PPNode_DOM ) {
1410 $root = $root->node;
1411 }
1412 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1413 $root = [ $root ];
1414 }
1415 foreach ( $root as $node ) {
1416 if ( $first ) {
1417 $first = false;
1418 } else {
1419 $s .= $sep;
1420 }
1421 $s .= $this->expand( $node );
1422 }
1423 }
1424 return $s;
1425 }
1426
1435 public function virtualImplode( $sep /*, ... */ ) {
1436 $args = array_slice( func_get_args(), 1 );
1437 $out = [];
1438 $first = true;
1439
1440 foreach ( $args as $root ) {
1441 if ( $root instanceof PPNode_DOM ) {
1442 $root = $root->node;
1443 }
1444 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1445 $root = [ $root ];
1446 }
1447 foreach ( $root as $node ) {
1448 if ( $first ) {
1449 $first = false;
1450 } else {
1451 $out[] = $sep;
1452 }
1453 $out[] = $node;
1454 }
1455 }
1456 return $out;
1457 }
1458
1467 public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1468 $args = array_slice( func_get_args(), 3 );
1469 $out = [ $start ];
1470 $first = true;
1471
1472 foreach ( $args as $root ) {
1473 if ( $root instanceof PPNode_DOM ) {
1474 $root = $root->node;
1475 }
1476 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1477 $root = [ $root ];
1478 }
1479 foreach ( $root as $node ) {
1480 if ( $first ) {
1481 $first = false;
1482 } else {
1483 $out[] = $sep;
1484 }
1485 $out[] = $node;
1486 }
1487 }
1488 $out[] = $end;
1489 return $out;
1490 }
1491
1492 public function __toString() {
1493 return 'frame{}';
1494 }
1495
1496 public function getPDBK( $level = false ) {
1497 if ( $level === false ) {
1498 return $this->title->getPrefixedDBkey();
1499 } else {
1500 return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
1501 }
1502 }
1503
1507 public function getArguments() {
1508 return [];
1509 }
1510
1514 public function getNumberedArguments() {
1515 return [];
1516 }
1517
1521 public function getNamedArguments() {
1522 return [];
1523 }
1524
1530 public function isEmpty() {
1531 return true;
1532 }
1533
1538 public function getArgument( $name ) {
1539 return false;
1540 }
1541
1548 public function loopCheck( $title ) {
1549 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1550 }
1551
1557 public function isTemplate() {
1558 return false;
1559 }
1560
1566 public function getTitle() {
1567 return $this->title;
1568 }
1569
1575 public function setVolatile( $flag = true ) {
1576 $this->volatile = $flag;
1577 }
1578
1584 public function isVolatile() {
1585 return $this->volatile;
1586 }
1587
1593 public function setTTL( $ttl ) {
1594 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1595 $this->ttl = $ttl;
1596 }
1597 }
1598
1604 public function getTTL() {
1605 return $this->ttl;
1606 }
1607}
1608
1613// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1614class PPTemplateFrame_DOM extends PPFrame_DOM {
1615 // @codingStandardsIgnoreEnd
1616
1617 public $numberedArgs, $namedArgs;
1618
1622 public $parent;
1623 public $numberedExpansionCache, $namedExpansionCache;
1624
1632 public function __construct( $preprocessor, $parent = false, $numberedArgs = [],
1633 $namedArgs = [], $title = false
1634 ) {
1635 parent::__construct( $preprocessor );
1636
1637 $this->parent = $parent;
1638 $this->numberedArgs = $numberedArgs;
1639 $this->namedArgs = $namedArgs;
1640 $this->title = $title;
1641 $pdbk = $title ? $title->getPrefixedDBkey() : false;
1642 $this->titleCache = $parent->titleCache;
1643 $this->titleCache[] = $pdbk;
1644 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1645 if ( $pdbk !== false ) {
1646 $this->loopCheckHash[$pdbk] = true;
1647 }
1648 $this->depth = $parent->depth + 1;
1649 $this->numberedExpansionCache = $this->namedExpansionCache = [];
1650 }
1651
1652 public function __toString() {
1653 $s = 'tplframe{';
1654 $first = true;
1655 $args = $this->numberedArgs + $this->namedArgs;
1656 foreach ( $args as $name => $value ) {
1657 if ( $first ) {
1658 $first = false;
1659 } else {
1660 $s .= ', ';
1661 }
1662 $s .= "\"$name\":\"" .
1663 str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"';
1664 }
1665 $s .= '}';
1666 return $s;
1667 }
1668
1676 public function cachedExpand( $key, $root, $flags = 0 ) {
1677 if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1678 return $this->parent->childExpansionCache[$key];
1679 }
1680 $retval = $this->expand( $root, $flags );
1681 if ( !$this->isVolatile() ) {
1682 $this->parent->childExpansionCache[$key] = $retval;
1683 }
1684 return $retval;
1685 }
1686
1692 public function isEmpty() {
1693 return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1694 }
1695
1696 public function getArguments() {
1697 $arguments = [];
1698 foreach ( array_merge(
1699 array_keys( $this->numberedArgs ),
1700 array_keys( $this->namedArgs ) ) as $key ) {
1701 $arguments[$key] = $this->getArgument( $key );
1702 }
1703 return $arguments;
1704 }
1705
1706 public function getNumberedArguments() {
1707 $arguments = [];
1708 foreach ( array_keys( $this->numberedArgs ) as $key ) {
1709 $arguments[$key] = $this->getArgument( $key );
1710 }
1711 return $arguments;
1712 }
1713
1714 public function getNamedArguments() {
1715 $arguments = [];
1716 foreach ( array_keys( $this->namedArgs ) as $key ) {
1717 $arguments[$key] = $this->getArgument( $key );
1718 }
1719 return $arguments;
1720 }
1721
1726 public function getNumberedArgument( $index ) {
1727 if ( !isset( $this->numberedArgs[$index] ) ) {
1728 return false;
1729 }
1730 if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1731 # No trimming for unnamed arguments
1732 $this->numberedExpansionCache[$index] = $this->parent->expand(
1733 $this->numberedArgs[$index],
1734 PPFrame::STRIP_COMMENTS
1735 );
1736 }
1737 return $this->numberedExpansionCache[$index];
1738 }
1739
1744 public function getNamedArgument( $name ) {
1745 if ( !isset( $this->namedArgs[$name] ) ) {
1746 return false;
1747 }
1748 if ( !isset( $this->namedExpansionCache[$name] ) ) {
1749 # Trim named arguments post-expand, for backwards compatibility
1750 $this->namedExpansionCache[$name] = trim(
1751 $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1752 }
1753 return $this->namedExpansionCache[$name];
1754 }
1755
1760 public function getArgument( $name ) {
1761 $text = $this->getNumberedArgument( $name );
1762 if ( $text === false ) {
1763 $text = $this->getNamedArgument( $name );
1764 }
1765 return $text;
1766 }
1767
1773 public function isTemplate() {
1774 return true;
1775 }
1776
1777 public function setVolatile( $flag = true ) {
1778 parent::setVolatile( $flag );
1779 $this->parent->setVolatile( $flag );
1780 }
1781
1782 public function setTTL( $ttl ) {
1783 parent::setTTL( $ttl );
1784 $this->parent->setTTL( $ttl );
1785 }
1786}
1787
1792// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1793class PPCustomFrame_DOM extends PPFrame_DOM {
1794 // @codingStandardsIgnoreEnd
1795
1796 public $args;
1797
1798 public function __construct( $preprocessor, $args ) {
1799 parent::__construct( $preprocessor );
1800 $this->args = $args;
1801 }
1802
1803 public function __toString() {
1804 $s = 'cstmframe{';
1805 $first = true;
1806 foreach ( $this->args as $name => $value ) {
1807 if ( $first ) {
1808 $first = false;
1809 } else {
1810 $s .= ', ';
1811 }
1812 $s .= "\"$name\":\"" .
1813 str_replace( '"', '\\"', $value->__toString() ) . '"';
1814 }
1815 $s .= '}';
1816 return $s;
1817 }
1818
1822 public function isEmpty() {
1823 return !count( $this->args );
1824 }
1825
1830 public function getArgument( $index ) {
1831 if ( !isset( $this->args[$index] ) ) {
1832 return false;
1833 }
1834 return $this->args[$index];
1835 }
1836
1837 public function getArguments() {
1838 return $this->args;
1839 }
1840}
1841
1845// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1846class PPNode_DOM implements PPNode {
1847 // @codingStandardsIgnoreEnd
1848
1852 public $node;
1853 public $xpath;
1854
1855 public function __construct( $node, $xpath = false ) {
1856 $this->node = $node;
1857 }
1858
1862 public function getXPath() {
1863 if ( $this->xpath === null ) {
1864 $this->xpath = new DOMXPath( $this->node->ownerDocument );
1865 }
1866 return $this->xpath;
1867 }
1868
1869 public function __toString() {
1870 if ( $this->node instanceof DOMNodeList ) {
1871 $s = '';
1872 foreach ( $this->node as $node ) {
1873 $s .= $node->ownerDocument->saveXML( $node );
1874 }
1875 } else {
1876 $s = $this->node->ownerDocument->saveXML( $this->node );
1877 }
1878 return $s;
1879 }
1880
1884 public function getChildren() {
1885 return $this->node->childNodes ? new self( $this->node->childNodes ) : false;
1886 }
1887
1891 public function getFirstChild() {
1892 return $this->node->firstChild ? new self( $this->node->firstChild ) : false;
1893 }
1894
1898 public function getNextSibling() {
1899 return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false;
1900 }
1901
1907 public function getChildrenOfType( $type ) {
1908 return new self( $this->getXPath()->query( $type, $this->node ) );
1909 }
1910
1914 public function getLength() {
1915 if ( $this->node instanceof DOMNodeList ) {
1916 return $this->node->length;
1917 } else {
1918 return false;
1919 }
1920 }
1921
1926 public function item( $i ) {
1927 $item = $this->node->item( $i );
1928 return $item ? new self( $item ) : false;
1929 }
1930
1934 public function getName() {
1935 if ( $this->node instanceof DOMNodeList ) {
1936 return '#nodelist';
1937 } else {
1938 return $this->node->nodeName;
1939 }
1940 }
1941
1951 public function splitArg() {
1952 $xpath = $this->getXPath();
1953 $names = $xpath->query( 'name', $this->node );
1954 $values = $xpath->query( 'value', $this->node );
1955 if ( !$names->length || !$values->length ) {
1956 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
1957 }
1958 $name = $names->item( 0 );
1959 $index = $name->getAttribute( 'index' );
1960 return [
1961 'name' => new self( $name ),
1962 'index' => $index,
1963 'value' => new self( $values->item( 0 ) ) ];
1964 }
1965
1973 public function splitExt() {
1974 $xpath = $this->getXPath();
1975 $names = $xpath->query( 'name', $this->node );
1976 $attrs = $xpath->query( 'attr', $this->node );
1977 $inners = $xpath->query( 'inner', $this->node );
1978 $closes = $xpath->query( 'close', $this->node );
1979 if ( !$names->length || !$attrs->length ) {
1980 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
1981 }
1982 $parts = [
1983 'name' => new self( $names->item( 0 ) ),
1984 'attr' => new self( $attrs->item( 0 ) ) ];
1985 if ( $inners->length ) {
1986 $parts['inner'] = new self( $inners->item( 0 ) );
1987 }
1988 if ( $closes->length ) {
1989 $parts['close'] = new self( $closes->item( 0 ) );
1990 }
1991 return $parts;
1992 }
1993
1999 public function splitHeading() {
2000 if ( $this->getName() !== 'h' ) {
2001 throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
2002 }
2003 return [
2004 'i' => $this->node->getAttribute( 'i' ),
2005 'level' => $this->node->getAttribute( 'level' ),
2006 'contents' => $this->getChildren()
2007 ];
2008 }
2009}
$wgDisableLangConversion
Whether to enable language variant conversion.
if( $line===false) $args
Definition cdb.php:63
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()
getChildrenOfType( $type)
__construct( $node, $xpath=false)
splitArg()
Split a "<part>" node into an associative array containing:
splitHeading()
Split a "<h>" node.
splitExt()
Split an "<ext>" node into an associative array containing name, attr, inner and close All values in ...
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:70
preprocessToXml( $text, $flags=0)
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.
cacheSetTree( $text, $flags, $tree)
Store a document tree in the cache.
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:1411
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition hooks.txt:2805
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
There are three types of nodes:
This document describes the state of Postgres support in and is fairly well maintained The main code is very well while extensions are very hit and miss it is probably the most supported database after MySQL Much of the work in making MediaWiki database agnostic came about through the work of creating Postgres as and are nearing end of but without copying over all the usage comments General notes on the but these can almost always be programmed around *Although Postgres has a true BOOLEAN type
Definition postgres.txt:36