MediaWiki REL1_31
Preprocessor_DOM.php
Go to the documentation of this file.
1<?php
27// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
29
33 public $parser;
34
36
37 const CACHE_PREFIX = 'preprocess-xml';
38
39 public function __construct( $parser ) {
40 $this->parser = $parser;
41 $mem = ini_get( 'memory_limit' );
42 $this->memoryLimit = false;
43 if ( strval( $mem ) !== '' && $mem != -1 ) {
44 if ( preg_match( '/^\d+$/', $mem ) ) {
45 $this->memoryLimit = $mem;
46 } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) {
47 $this->memoryLimit = $m[1] * 1048576;
48 }
49 }
50 }
51
55 public function newFrame() {
56 return new PPFrame_DOM( $this );
57 }
58
63 public function newCustomFrame( $args ) {
64 return new PPCustomFrame_DOM( $this, $args );
65 }
66
72 public function newPartNodeArray( $values ) {
73 // NOTE: DOM manipulation is slower than building & parsing XML! (or so Tim sais)
74 $xml = "<list>";
75
76 foreach ( $values as $k => $val ) {
77 if ( is_int( $k ) ) {
78 $xml .= "<part><name index=\"$k\"/><value>"
79 . htmlspecialchars( $val ) . "</value></part>";
80 } else {
81 $xml .= "<part><name>" . htmlspecialchars( $k )
82 . "</name>=<value>" . htmlspecialchars( $val ) . "</value></part>";
83 }
84 }
85
86 $xml .= "</list>";
87
88 $dom = new DOMDocument();
89 Wikimedia\suppressWarnings();
90 $result = $dom->loadXML( $xml );
91 Wikimedia\restoreWarnings();
92 if ( !$result ) {
93 // Try running the XML through UtfNormal to get rid of invalid characters
94 $xml = UtfNormal\Validator::cleanUp( $xml );
95 // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2
96 // don't barf when the XML is >256 levels deep
97 $result = $dom->loadXML( $xml, 1 << 19 );
98 }
99
100 if ( !$result ) {
101 throw new MWException( 'Parameters passed to ' . __METHOD__ . ' result in invalid XML' );
102 }
103
104 $root = $dom->documentElement;
105 $node = new PPNode_DOM( $root->childNodes );
106 return $node;
107 }
108
113 public function memCheck() {
114 if ( $this->memoryLimit === false ) {
115 return true;
116 }
117 $usage = memory_get_usage();
118 if ( $usage > $this->memoryLimit * 0.9 ) {
119 $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 );
120 throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" );
121 }
122 return $usage <= $this->memoryLimit * 0.8;
123 }
124
149 public function preprocessToObj( $text, $flags = 0 ) {
150 $xml = $this->cacheGetTree( $text, $flags );
151 if ( $xml === false ) {
152 $xml = $this->preprocessToXml( $text, $flags );
153 $this->cacheSetTree( $text, $flags, $xml );
154 }
155
156 // Fail if the number of elements exceeds acceptable limits
157 // Do not attempt to generate the DOM
158 $this->parser->mGeneratedPPNodeCount += substr_count( $xml, '<' );
159 $max = $this->parser->mOptions->getMaxGeneratedPPNodeCount();
160 if ( $this->parser->mGeneratedPPNodeCount > $max ) {
161 // if ( $cacheable ) { ... }
162 throw new MWException( __METHOD__ . ': generated node count limit exceeded' );
163 }
164
165 $dom = new DOMDocument;
166 Wikimedia\suppressWarnings();
167 $result = $dom->loadXML( $xml );
168 Wikimedia\restoreWarnings();
169 if ( !$result ) {
170 // Try running the XML through UtfNormal to get rid of invalid characters
171 $xml = UtfNormal\Validator::cleanUp( $xml );
172 // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2
173 // don't barf when the XML is >256 levels deep.
174 $result = $dom->loadXML( $xml, 1 << 19 );
175 }
176 if ( $result ) {
177 $obj = new PPNode_DOM( $dom->documentElement );
178 }
179
180 // if ( $cacheable ) { ... }
181
182 if ( !$result ) {
183 throw new MWException( __METHOD__ . ' generated invalid XML' );
184 }
185 return $obj;
186 }
187
193 public function preprocessToXml( $text, $flags = 0 ) {
195
196 $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
197
198 $xmlishElements = $this->parser->getStripList();
199 $xmlishAllowMissingEndTag = [ 'includeonly', 'noinclude', 'onlyinclude' ];
200 $enableOnlyinclude = false;
201 if ( $forInclusion ) {
202 $ignoredTags = [ 'includeonly', '/includeonly' ];
203 $ignoredElements = [ 'noinclude' ];
204 $xmlishElements[] = 'noinclude';
205 if ( strpos( $text, '<onlyinclude>' ) !== false
206 && strpos( $text, '</onlyinclude>' ) !== false
207 ) {
208 $enableOnlyinclude = true;
209 }
210 } else {
211 $ignoredTags = [ 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ];
212 $ignoredElements = [ 'includeonly' ];
213 $xmlishElements[] = 'includeonly';
214 }
215 $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
216
217 // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
218 $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
219
220 $stack = new PPDStack;
221
222 $searchBase = "[{<\n"; # }
224 $searchBase .= '-';
225 }
226
227 // For fast reverse searches
228 $revText = strrev( $text );
229 $lengthText = strlen( $text );
230
231 // Input pointer, starts out pointing to a pseudo-newline before the start
232 $i = 0;
233 // Current accumulator
234 $accum =& $stack->getAccum();
235 $accum = '<root>';
236 // True to find equals signs in arguments
237 $findEquals = false;
238 // True to take notice of pipe characters
239 $findPipe = false;
240 $headingIndex = 1;
241 // True if $i is inside a possible heading
242 $inHeading = false;
243 // True if there are no more greater-than (>) signs right of $i
244 $noMoreGT = false;
245 // Map of tag name => true if there are no more closing tags of given type right of $i
246 $noMoreClosingTag = [];
247 // True to ignore all input up to the next <onlyinclude>
248 $findOnlyinclude = $enableOnlyinclude;
249 // Do a line-start run without outputting an LF character
250 $fakeLineStart = true;
251
252 while ( true ) {
253 // $this->memCheck();
254
255 if ( $findOnlyinclude ) {
256 // Ignore all input up to the next <onlyinclude>
257 $startPos = strpos( $text, '<onlyinclude>', $i );
258 if ( $startPos === false ) {
259 // Ignored section runs to the end
260 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>';
261 break;
262 }
263 $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
264 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>';
265 $i = $tagEndPos;
266 $findOnlyinclude = false;
267 }
268
269 if ( $fakeLineStart ) {
270 $found = 'line-start';
271 $curChar = '';
272 } else {
273 # Find next opening brace, closing brace or pipe
274 $search = $searchBase;
275 if ( $stack->top === false ) {
276 $currentClosing = '';
277 } else {
278 $currentClosing = $stack->top->close;
279 $search .= $currentClosing;
280 }
281 if ( $findPipe ) {
282 $search .= '|';
283 }
284 if ( $findEquals ) {
285 // First equals will be for the template
286 $search .= '=';
287 }
288 $rule = null;
289 # Output literal section, advance input counter
290 $literalLength = strcspn( $text, $search, $i );
291 if ( $literalLength > 0 ) {
292 $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) );
293 $i += $literalLength;
294 }
295 if ( $i >= $lengthText ) {
296 if ( $currentClosing == "\n" ) {
297 // Do a past-the-end run to finish off the heading
298 $curChar = '';
299 $found = 'line-end';
300 } else {
301 # All done
302 break;
303 }
304 } else {
305 $curChar = $curTwoChar = $text[$i];
306 if ( ( $i + 1 ) < $lengthText ) {
307 $curTwoChar .= $text[$i + 1];
308 }
309 if ( $curChar == '|' ) {
310 $found = 'pipe';
311 } elseif ( $curChar == '=' ) {
312 $found = 'equals';
313 } elseif ( $curChar == '<' ) {
314 $found = 'angle';
315 } elseif ( $curChar == "\n" ) {
316 if ( $inHeading ) {
317 $found = 'line-end';
318 } else {
319 $found = 'line-start';
320 }
321 } elseif ( $curTwoChar == $currentClosing ) {
322 $found = 'close';
323 $curChar = $curTwoChar;
324 } elseif ( $curChar == $currentClosing ) {
325 $found = 'close';
326 } elseif ( isset( $this->rules[$curTwoChar] ) ) {
327 $curChar = $curTwoChar;
328 $found = 'open';
329 $rule = $this->rules[$curChar];
330 } elseif ( isset( $this->rules[$curChar] ) ) {
331 $found = 'open';
332 $rule = $this->rules[$curChar];
333 } else {
334 # Some versions of PHP have a strcspn which stops on
335 # null characters; ignore these and continue.
336 # We also may get '-' and '}' characters here which
337 # don't match -{ or $currentClosing. Add these to
338 # output and continue.
339 if ( $curChar == '-' || $curChar == '}' ) {
340 $accum .= $curChar;
341 }
342 ++$i;
343 continue;
344 }
345 }
346 }
347
348 if ( $found == 'angle' ) {
349 $matches = false;
350 // Handle </onlyinclude>
351 if ( $enableOnlyinclude
352 && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>'
353 ) {
354 $findOnlyinclude = true;
355 continue;
356 }
357
358 // Determine element name
359 if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
360 // Element name missing or not listed
361 $accum .= '&lt;';
362 ++$i;
363 continue;
364 }
365 // Handle comments
366 if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
367 // To avoid leaving blank lines, when a sequence of
368 // space-separated comments is both preceded and followed by
369 // a newline (ignoring spaces), then
370 // trim leading and trailing spaces and the trailing newline.
371
372 // Find the end
373 $endPos = strpos( $text, '-->', $i + 4 );
374 if ( $endPos === false ) {
375 // Unclosed comment in input, runs to end
376 $inner = substr( $text, $i );
377 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
378 $i = $lengthText;
379 } else {
380 // Search backwards for leading whitespace
381 $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0;
382
383 // Search forwards for trailing whitespace
384 // $wsEnd will be the position of the last space (or the '>' if there's none)
385 $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 );
386
387 // Keep looking forward as long as we're finding more
388 // comments.
389 $comments = [ [ $wsStart, $wsEnd ] ];
390 while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) {
391 $c = strpos( $text, '-->', $wsEnd + 4 );
392 if ( $c === false ) {
393 break;
394 }
395 $c = $c + 2 + strspn( $text, " \t", $c + 3 );
396 $comments[] = [ $wsEnd + 1, $c ];
397 $wsEnd = $c;
398 }
399
400 // Eat the line if possible
401 // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
402 // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
403 // it's a possible beneficial b/c break.
404 if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
405 && substr( $text, $wsEnd + 1, 1 ) == "\n"
406 ) {
407 // Remove leading whitespace from the end of the accumulator
408 // Sanity check first though
409 $wsLength = $i - $wsStart;
410 if ( $wsLength > 0
411 && strspn( $accum, " \t", -$wsLength ) === $wsLength
412 ) {
413 $accum = substr( $accum, 0, -$wsLength );
414 }
415
416 // Dump all but the last comment to the accumulator
417 foreach ( $comments as $j => $com ) {
418 $startPos = $com[0];
419 $endPos = $com[1] + 1;
420 if ( $j == ( count( $comments ) - 1 ) ) {
421 break;
422 }
423 $inner = substr( $text, $startPos, $endPos - $startPos );
424 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
425 }
426
427 // Do a line-start run next time to look for headings after the comment
428 $fakeLineStart = true;
429 } else {
430 // No line to eat, just take the comment itself
431 $startPos = $i;
432 $endPos += 2;
433 }
434
435 if ( $stack->top ) {
436 $part = $stack->top->getCurrentPart();
437 if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) {
438 $part->visualEnd = $wsStart;
439 }
440 // Else comments abutting, no change in visual end
441 $part->commentEnd = $endPos;
442 }
443 $i = $endPos + 1;
444 $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
445 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
446 }
447 continue;
448 }
449 $name = $matches[1];
450 $lowerName = strtolower( $name );
451 $attrStart = $i + strlen( $name ) + 1;
452
453 // Find end of tag
454 $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
455 if ( $tagEndPos === false ) {
456 // Infinite backtrack
457 // Disable tag search to prevent worst-case O(N^2) performance
458 $noMoreGT = true;
459 $accum .= '&lt;';
460 ++$i;
461 continue;
462 }
463
464 // Handle ignored tags
465 if ( in_array( $lowerName, $ignoredTags ) ) {
466 $accum .= '<ignore>'
467 . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) )
468 . '</ignore>';
469 $i = $tagEndPos + 1;
470 continue;
471 }
472
473 $tagStartPos = $i;
474 if ( $text[$tagEndPos - 1] == '/' ) {
475 $attrEnd = $tagEndPos - 1;
476 $inner = null;
477 $i = $tagEndPos + 1;
478 $close = '';
479 } else {
480 $attrEnd = $tagEndPos;
481 // Find closing tag
482 if (
483 !isset( $noMoreClosingTag[$name] ) &&
484 preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
485 $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 )
486 ) {
487 $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
488 $i = $matches[0][1] + strlen( $matches[0][0] );
489 $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>';
490 } else {
491 // No end tag
492 if ( in_array( $name, $xmlishAllowMissingEndTag ) ) {
493 // Let it run out to the end of the text.
494 $inner = substr( $text, $tagEndPos + 1 );
495 $i = $lengthText;
496 $close = '';
497 } else {
498 // Don't match the tag, treat opening tag as literal and resume parsing.
499 $i = $tagEndPos + 1;
500 $accum .= htmlspecialchars( substr( $text, $tagStartPos, $tagEndPos + 1 - $tagStartPos ) );
501 // Cache results, otherwise we have O(N^2) performance for input like <foo><foo><foo>...
502 $noMoreClosingTag[$name] = true;
503 continue;
504 }
505 }
506 }
507 // <includeonly> and <noinclude> just become <ignore> tags
508 if ( in_array( $lowerName, $ignoredElements ) ) {
509 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) )
510 . '</ignore>';
511 continue;
512 }
513
514 $accum .= '<ext>';
515 if ( $attrEnd <= $attrStart ) {
516 $attr = '';
517 } else {
518 $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
519 }
520 $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' .
521 // Note that the attr element contains the whitespace between name and attribute,
522 // this is necessary for precise reconstruction during pre-save transform.
523 '<attr>' . htmlspecialchars( $attr ) . '</attr>';
524 if ( $inner !== null ) {
525 $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>';
526 }
527 $accum .= $close . '</ext>';
528 } elseif ( $found == 'line-start' ) {
529 // Is this the start of a heading?
530 // Line break belongs before the heading element in any case
531 if ( $fakeLineStart ) {
532 $fakeLineStart = false;
533 } else {
534 $accum .= $curChar;
535 $i++;
536 }
537
538 $count = strspn( $text, '=', $i, 6 );
539 if ( $count == 1 && $findEquals ) {
540 // DWIM: This looks kind of like a name/value separator.
541 // Let's let the equals handler have it and break the
542 // potential heading. This is heuristic, but AFAICT the
543 // methods for completely correct disambiguation are very
544 // complex.
545 } elseif ( $count > 0 ) {
546 $piece = [
547 'open' => "\n",
548 'close' => "\n",
549 'parts' => [ new PPDPart( str_repeat( '=', $count ) ) ],
550 'startPos' => $i,
551 'count' => $count ];
552 $stack->push( $piece );
553 $accum =& $stack->getAccum();
554 $stackFlags = $stack->getFlags();
555 if ( isset( $stackFlags['findEquals'] ) ) {
556 $findEquals = $stackFlags['findEquals'];
557 }
558 if ( isset( $stackFlags['findPipe'] ) ) {
559 $findPipe = $stackFlags['findPipe'];
560 }
561 if ( isset( $stackFlags['inHeading'] ) ) {
562 $inHeading = $stackFlags['inHeading'];
563 }
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 $stackFlags = $stack->getFlags();
614 if ( isset( $stackFlags['findEquals'] ) ) {
615 $findEquals = $stackFlags['findEquals'];
616 }
617 if ( isset( $stackFlags['findPipe'] ) ) {
618 $findPipe = $stackFlags['findPipe'];
619 }
620 if ( isset( $stackFlags['inHeading'] ) ) {
621 $inHeading = $stackFlags['inHeading'];
622 }
623
624 // Append the result to the enclosing accumulator
625 $accum .= $element;
626 // Note that we do NOT increment the input pointer.
627 // This is because the closing linebreak could be the opening linebreak of
628 // another heading. Infinite loops are avoided because the next iteration MUST
629 // hit the heading open case above, which unconditionally increments the
630 // input pointer.
631 } elseif ( $found == 'open' ) {
632 # count opening brace characters
633 $curLen = strlen( $curChar );
634 $count = ( $curLen > 1 ) ?
635 # allow the final character to repeat
636 strspn( $text, $curChar[$curLen - 1], $i + 1 ) + 1 :
637 strspn( $text, $curChar, $i );
638
639 $savedPrefix = '';
640 $lineStart = ( $i > 0 && $text[$i - 1] == "\n" );
641
642 if ( $curChar === "-{" && $count > $curLen ) {
643 // -{ => {{ transition because rightmost wins
644 $savedPrefix = '-';
645 $i++;
646 $curChar = '{';
647 $count--;
648 $rule = $this->rules[$curChar];
649 }
650
651 # we need to add to stack only if opening brace count is enough for one of the rules
652 if ( $count >= $rule['min'] ) {
653 # Add it to the stack
654 $piece = [
655 'open' => $curChar,
656 'close' => $rule['end'],
657 'savedPrefix' => $savedPrefix,
658 'count' => $count,
659 'lineStart' => $lineStart,
660 ];
661
662 $stack->push( $piece );
663 $accum =& $stack->getAccum();
664 $stackFlags = $stack->getFlags();
665 if ( isset( $stackFlags['findEquals'] ) ) {
666 $findEquals = $stackFlags['findEquals'];
667 }
668 if ( isset( $stackFlags['findPipe'] ) ) {
669 $findPipe = $stackFlags['findPipe'];
670 }
671 if ( isset( $stackFlags['inHeading'] ) ) {
672 $inHeading = $stackFlags['inHeading'];
673 }
674 } else {
675 # Add literal brace(s)
676 $accum .= htmlspecialchars( $savedPrefix . str_repeat( $curChar, $count ) );
677 }
678 $i += $count;
679 } elseif ( $found == 'close' ) {
680 $piece = $stack->top;
681 # lets check if there are enough characters for closing brace
682 $maxCount = $piece->count;
683 if ( $piece->close === '}-' && $curChar === '}' ) {
684 $maxCount--; # don't try to match closing '-' as a '}'
685 }
686 $curLen = strlen( $curChar );
687 $count = ( $curLen > 1 ) ? $curLen :
688 strspn( $text, $curChar, $i, $maxCount );
689
690 # check for maximum matching characters (if there are 5 closing
691 # characters, we will probably need only 3 - depending on the rules)
692 $rule = $this->rules[$piece->open];
693 if ( $count > $rule['max'] ) {
694 # The specified maximum exists in the callback array, unless the caller
695 # has made an error
696 $matchingCount = $rule['max'];
697 } else {
698 # Count is less than the maximum
699 # Skip any gaps in the callback array to find the true largest match
700 # Need to use array_key_exists not isset because the callback can be null
701 $matchingCount = $count;
702 while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
703 --$matchingCount;
704 }
705 }
706
707 if ( $matchingCount <= 0 ) {
708 # No matching element found in callback array
709 # Output a literal closing brace and continue
710 $endText = substr( $text, $i, $count );
711 $accum .= htmlspecialchars( $endText );
712 $i += $count;
713 continue;
714 }
715 $name = $rule['names'][$matchingCount];
716 if ( $name === null ) {
717 // No element, just literal text
718 $endText = substr( $text, $i, $matchingCount );
719 $element = $piece->breakSyntax( $matchingCount ) . $endText;
720 } else {
721 # Create XML element
722 # Note: $parts is already XML, does not need to be encoded further
723 $parts = $piece->parts;
724 $title = $parts[0]->out;
725 unset( $parts[0] );
726
727 # The invocation is at the start of the line if lineStart is set in
728 # the stack, and all opening brackets are used up.
729 if ( $maxCount == $matchingCount &&
730 !empty( $piece->lineStart ) &&
731 strlen( $piece->savedPrefix ) == 0 ) {
732 $attr = ' lineStart="1"';
733 } else {
734 $attr = '';
735 }
736
737 $element = "<$name$attr>";
738 $element .= "<title>$title</title>";
739 $argIndex = 1;
740 foreach ( $parts as $part ) {
741 if ( isset( $part->eqpos ) ) {
742 $argName = substr( $part->out, 0, $part->eqpos );
743 $argValue = substr( $part->out, $part->eqpos + 1 );
744 $element .= "<part><name>$argName</name>=<value>$argValue</value></part>";
745 } else {
746 $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>";
747 $argIndex++;
748 }
749 }
750 $element .= "</$name>";
751 }
752
753 # Advance input pointer
754 $i += $matchingCount;
755
756 # Unwind the stack
757 $stack->pop();
758 $accum =& $stack->getAccum();
759
760 # Re-add the old stack element if it still has unmatched opening characters remaining
761 if ( $matchingCount < $piece->count ) {
762 $piece->parts = [ new PPDPart ];
763 $piece->count -= $matchingCount;
764 # do we still qualify for any callback with remaining count?
765 $min = $this->rules[$piece->open]['min'];
766 if ( $piece->count >= $min ) {
767 $stack->push( $piece );
768 $accum =& $stack->getAccum();
769 } elseif ( $piece->count == 1 && $piece->open === '{' && $piece->savedPrefix === '-' ) {
770 $piece->savedPrefix = '';
771 $piece->open = '-{';
772 $piece->count = 2;
773 $piece->close = $this->rules[$piece->open]['end'];
774 $stack->push( $piece );
775 $accum =& $stack->getAccum();
776 } else {
777 $s = substr( $piece->open, 0, -1 );
778 $s .= str_repeat(
779 substr( $piece->open, -1 ),
780 $piece->count - strlen( $s )
781 );
782 $accum .= $piece->savedPrefix . $s;
783 }
784 } elseif ( $piece->savedPrefix !== '' ) {
785 $accum .= $piece->savedPrefix;
786 }
787
788 $stackFlags = $stack->getFlags();
789 if ( isset( $stackFlags['findEquals'] ) ) {
790 $findEquals = $stackFlags['findEquals'];
791 }
792 if ( isset( $stackFlags['findPipe'] ) ) {
793 $findPipe = $stackFlags['findPipe'];
794 }
795 if ( isset( $stackFlags['inHeading'] ) ) {
796 $inHeading = $stackFlags['inHeading'];
797 }
798
799 # Add XML element to the enclosing accumulator
800 $accum .= $element;
801 } elseif ( $found == 'pipe' ) {
802 $findEquals = true; // shortcut for getFlags()
803 $stack->addPart();
804 $accum =& $stack->getAccum();
805 ++$i;
806 } elseif ( $found == 'equals' ) {
807 $findEquals = false; // shortcut for getFlags()
808 $stack->getCurrentPart()->eqpos = strlen( $accum );
809 $accum .= '=';
810 ++$i;
811 }
812 }
813
814 # Output any remaining unclosed brackets
815 foreach ( $stack->stack as $piece ) {
816 $stack->rootAccum .= $piece->breakSyntax();
817 }
818 $stack->rootAccum .= '</root>';
819 $xml = $stack->rootAccum;
820
821 return $xml;
822 }
823}
824
829class PPDStack {
830 public $stack, $rootAccum;
831
835 public $top;
836 public $out;
837 public $elementClass = PPDStackElement::class;
838
839 public static $false = false;
840
841 public function __construct() {
842 $this->stack = [];
843 $this->top = false;
844 $this->rootAccum = '';
845 $this->accum =& $this->rootAccum;
846 }
847
851 public function count() {
852 return count( $this->stack );
853 }
854
855 public function &getAccum() {
856 return $this->accum;
857 }
858
859 public function getCurrentPart() {
860 if ( $this->top === false ) {
861 return false;
862 } else {
863 return $this->top->getCurrentPart();
864 }
865 }
866
867 public function push( $data ) {
868 if ( $data instanceof $this->elementClass ) {
869 $this->stack[] = $data;
870 } else {
871 $class = $this->elementClass;
872 $this->stack[] = new $class( $data );
873 }
874 $this->top = $this->stack[count( $this->stack ) - 1];
875 $this->accum =& $this->top->getAccum();
876 }
877
878 public function pop() {
879 if ( !count( $this->stack ) ) {
880 throw new MWException( __METHOD__ . ': no elements remaining' );
881 }
882 $temp = array_pop( $this->stack );
883
884 if ( count( $this->stack ) ) {
885 $this->top = $this->stack[count( $this->stack ) - 1];
886 $this->accum =& $this->top->getAccum();
887 } else {
888 $this->top = self::$false;
889 $this->accum =& $this->rootAccum;
890 }
891 return $temp;
892 }
893
894 public function addPart( $s = '' ) {
895 $this->top->addPart( $s );
896 $this->accum =& $this->top->getAccum();
897 }
898
902 public function getFlags() {
903 if ( !count( $this->stack ) ) {
904 return [
905 'findEquals' => false,
906 'findPipe' => false,
907 'inHeading' => false,
908 ];
909 } else {
910 return $this->top->getFlags();
911 }
912 }
913}
914
918class PPDStackElement {
922 public $open;
923
927 public $close;
928
933 public $savedPrefix = '';
934
938 public $count;
939
943 public $parts;
944
949 public $lineStart;
950
951 public $partClass = PPDPart::class;
952
953 public function __construct( $data = [] ) {
954 $class = $this->partClass;
955 $this->parts = [ new $class ];
956
957 foreach ( $data as $name => $value ) {
958 $this->$name = $value;
959 }
960 }
961
962 public function &getAccum() {
963 return $this->parts[count( $this->parts ) - 1]->out;
964 }
965
966 public function addPart( $s = '' ) {
967 $class = $this->partClass;
968 $this->parts[] = new $class( $s );
969 }
970
971 public function getCurrentPart() {
972 return $this->parts[count( $this->parts ) - 1];
973 }
974
978 public function getFlags() {
979 $partCount = count( $this->parts );
980 $findPipe = $this->open != "\n" && $this->open != '[';
981 return [
982 'findPipe' => $findPipe,
983 'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ),
984 'inHeading' => $this->open == "\n",
985 ];
986 }
987
994 public function breakSyntax( $openingCount = false ) {
995 if ( $this->open == "\n" ) {
996 $s = $this->savedPrefix . $this->parts[0]->out;
997 } else {
998 if ( $openingCount === false ) {
999 $openingCount = $this->count;
1000 }
1001 $s = substr( $this->open, 0, -1 );
1002 $s .= str_repeat(
1003 substr( $this->open, -1 ),
1004 $openingCount - strlen( $s )
1005 );
1006 $s = $this->savedPrefix . $s;
1007 $first = true;
1008 foreach ( $this->parts as $part ) {
1009 if ( $first ) {
1010 $first = false;
1011 } else {
1012 $s .= '|';
1013 }
1014 $s .= $part->out;
1015 }
1016 }
1017 return $s;
1018 }
1019}
1020
1024class PPDPart {
1028 public $out;
1029
1030 // Optional member variables:
1031 // eqpos Position of equals sign in output accumulator
1032 // commentEnd Past-the-end input pointer for the last comment encountered
1033 // visualEnd Past-the-end input pointer for the end of the accumulator minus comments
1034
1035 public function __construct( $out = '' ) {
1036 $this->out = $out;
1037 }
1038}
1039
1044// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1045class PPFrame_DOM implements PPFrame {
1046
1050 public $preprocessor;
1051
1055 public $parser;
1056
1060 public $title;
1061 public $titleCache;
1062
1067 public $loopCheckHash;
1068
1073 public $depth;
1074
1075 private $volatile = false;
1076 private $ttl = null;
1077
1081 protected $childExpansionCache;
1082
1087 public function __construct( $preprocessor ) {
1088 $this->preprocessor = $preprocessor;
1089 $this->parser = $preprocessor->parser;
1090 $this->title = $this->parser->mTitle;
1091 $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
1092 $this->loopCheckHash = [];
1093 $this->depth = 0;
1094 $this->childExpansionCache = [];
1095 }
1096
1106 public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
1107 $namedArgs = [];
1108 $numberedArgs = [];
1109 if ( $title === false ) {
1110 $title = $this->title;
1111 }
1112 if ( $args !== false ) {
1113 $xpath = false;
1114 if ( $args instanceof PPNode ) {
1115 $args = $args->node;
1116 }
1117 foreach ( $args as $arg ) {
1118 if ( $arg instanceof PPNode ) {
1119 $arg = $arg->node;
1120 }
1121 if ( !$xpath || $xpath->document !== $arg->ownerDocument ) {
1122 $xpath = new DOMXPath( $arg->ownerDocument );
1123 }
1124
1125 $nameNodes = $xpath->query( 'name', $arg );
1126 $value = $xpath->query( 'value', $arg );
1127 if ( $nameNodes->item( 0 )->hasAttributes() ) {
1128 // Numbered parameter
1129 $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
1130 $index = $index - $indexOffset;
1131 if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
1132 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
1133 wfEscapeWikiText( $this->title ),
1134 wfEscapeWikiText( $title ),
1135 wfEscapeWikiText( $index ) )->text() );
1136 $this->parser->addTrackingCategory( 'duplicate-args-category' );
1137 }
1138 $numberedArgs[$index] = $value->item( 0 );
1139 unset( $namedArgs[$index] );
1140 } else {
1141 // Named parameter
1142 $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) );
1143 if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
1144 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
1145 wfEscapeWikiText( $this->title ),
1146 wfEscapeWikiText( $title ),
1147 wfEscapeWikiText( $name ) )->text() );
1148 $this->parser->addTrackingCategory( 'duplicate-args-category' );
1149 }
1150 $namedArgs[$name] = $value->item( 0 );
1151 unset( $numberedArgs[$name] );
1152 }
1153 }
1154 }
1155 return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
1156 }
1157
1165 public function cachedExpand( $key, $root, $flags = 0 ) {
1166 // we don't have a parent, so we don't have a cache
1167 return $this->expand( $root, $flags );
1168 }
1169
1176 public function expand( $root, $flags = 0 ) {
1177 static $expansionDepth = 0;
1178 if ( is_string( $root ) ) {
1179 return $root;
1180 }
1181
1182 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
1183 $this->parser->limitationWarn( 'node-count-exceeded',
1184 $this->parser->mPPNodeCount,
1185 $this->parser->mOptions->getMaxPPNodeCount()
1186 );
1187 return '<span class="error">Node-count limit exceeded</span>';
1188 }
1189
1190 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
1191 $this->parser->limitationWarn( 'expansion-depth-exceeded',
1192 $expansionDepth,
1193 $this->parser->mOptions->getMaxPPExpandDepth()
1194 );
1195 return '<span class="error">Expansion depth limit exceeded</span>';
1196 }
1197 ++$expansionDepth;
1198 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
1199 $this->parser->mHighestExpansionDepth = $expansionDepth;
1200 }
1201
1202 if ( $root instanceof PPNode_DOM ) {
1203 $root = $root->node;
1204 }
1205 if ( $root instanceof DOMDocument ) {
1206 $root = $root->documentElement;
1207 }
1208
1209 $outStack = [ '', '' ];
1210 $iteratorStack = [ false, $root ];
1211 $indexStack = [ 0, 0 ];
1212
1213 while ( count( $iteratorStack ) > 1 ) {
1214 $level = count( $outStack ) - 1;
1215 $iteratorNode =& $iteratorStack[$level];
1216 $out =& $outStack[$level];
1217 $index =& $indexStack[$level];
1218
1219 if ( $iteratorNode instanceof PPNode_DOM ) {
1220 $iteratorNode = $iteratorNode->node;
1221 }
1222
1223 if ( is_array( $iteratorNode ) ) {
1224 if ( $index >= count( $iteratorNode ) ) {
1225 // All done with this iterator
1226 $iteratorStack[$level] = false;
1227 $contextNode = false;
1228 } else {
1229 $contextNode = $iteratorNode[$index];
1230 $index++;
1231 }
1232 } elseif ( $iteratorNode instanceof DOMNodeList ) {
1233 if ( $index >= $iteratorNode->length ) {
1234 // All done with this iterator
1235 $iteratorStack[$level] = false;
1236 $contextNode = false;
1237 } else {
1238 $contextNode = $iteratorNode->item( $index );
1239 $index++;
1240 }
1241 } else {
1242 // Copy to $contextNode and then delete from iterator stack,
1243 // because this is not an iterator but we do have to execute it once
1244 $contextNode = $iteratorStack[$level];
1245 $iteratorStack[$level] = false;
1246 }
1247
1248 if ( $contextNode instanceof PPNode_DOM ) {
1249 $contextNode = $contextNode->node;
1250 }
1251
1252 $newIterator = false;
1253
1254 if ( $contextNode === false ) {
1255 // nothing to do
1256 } elseif ( is_string( $contextNode ) ) {
1257 $out .= $contextNode;
1258 } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) {
1259 $newIterator = $contextNode;
1260 } elseif ( $contextNode instanceof DOMNode ) {
1261 if ( $contextNode->nodeType == XML_TEXT_NODE ) {
1262 $out .= $contextNode->nodeValue;
1263 } elseif ( $contextNode->nodeName == 'template' ) {
1264 # Double-brace expansion
1265 $xpath = new DOMXPath( $contextNode->ownerDocument );
1266 $titles = $xpath->query( 'title', $contextNode );
1267 $title = $titles->item( 0 );
1268 $parts = $xpath->query( 'part', $contextNode );
1269 if ( $flags & PPFrame::NO_TEMPLATES ) {
1270 $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
1271 } else {
1272 $lineStart = $contextNode->getAttribute( 'lineStart' );
1273 $params = [
1274 'title' => new PPNode_DOM( $title ),
1275 'parts' => new PPNode_DOM( $parts ),
1276 'lineStart' => $lineStart ];
1277 $ret = $this->parser->braceSubstitution( $params, $this );
1278 if ( isset( $ret['object'] ) ) {
1279 $newIterator = $ret['object'];
1280 } else {
1281 $out .= $ret['text'];
1282 }
1283 }
1284 } elseif ( $contextNode->nodeName == 'tplarg' ) {
1285 # Triple-brace expansion
1286 $xpath = new DOMXPath( $contextNode->ownerDocument );
1287 $titles = $xpath->query( 'title', $contextNode );
1288 $title = $titles->item( 0 );
1289 $parts = $xpath->query( 'part', $contextNode );
1290 if ( $flags & PPFrame::NO_ARGS ) {
1291 $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
1292 } else {
1293 $params = [
1294 'title' => new PPNode_DOM( $title ),
1295 'parts' => new PPNode_DOM( $parts ) ];
1296 $ret = $this->parser->argSubstitution( $params, $this );
1297 if ( isset( $ret['object'] ) ) {
1298 $newIterator = $ret['object'];
1299 } else {
1300 $out .= $ret['text'];
1301 }
1302 }
1303 } elseif ( $contextNode->nodeName == 'comment' ) {
1304 # HTML-style comment
1305 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1306 # Not in RECOVER_COMMENTS mode (msgnw) though.
1307 if ( ( $this->parser->ot['html']
1308 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1309 || ( $flags & PPFrame::STRIP_COMMENTS )
1310 ) && !( $flags & PPFrame::RECOVER_COMMENTS )
1311 ) {
1312 $out .= '';
1313 } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1314 # Add a strip marker in PST mode so that pstPass2() can
1315 # run some old-fashioned regexes on the result.
1316 # Not in RECOVER_COMMENTS mode (extractSections) though.
1317 $out .= $this->parser->insertStripItem( $contextNode->textContent );
1318 } else {
1319 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1320 $out .= $contextNode->textContent;
1321 }
1322 } elseif ( $contextNode->nodeName == 'ignore' ) {
1323 # Output suppression used by <includeonly> etc.
1324 # OT_WIKI will only respect <ignore> in substed templates.
1325 # The other output types respect it unless NO_IGNORE is set.
1326 # extractSections() sets NO_IGNORE and so never respects it.
1327 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1328 || ( $flags & PPFrame::NO_IGNORE )
1329 ) {
1330 $out .= $contextNode->textContent;
1331 } else {
1332 $out .= '';
1333 }
1334 } elseif ( $contextNode->nodeName == 'ext' ) {
1335 # Extension tag
1336 $xpath = new DOMXPath( $contextNode->ownerDocument );
1337 $names = $xpath->query( 'name', $contextNode );
1338 $attrs = $xpath->query( 'attr', $contextNode );
1339 $inners = $xpath->query( 'inner', $contextNode );
1340 $closes = $xpath->query( 'close', $contextNode );
1341 if ( $flags & PPFrame::NO_TAGS ) {
1342 $s = '<' . $this->expand( $names->item( 0 ), $flags );
1343 if ( $attrs->length > 0 ) {
1344 $s .= $this->expand( $attrs->item( 0 ), $flags );
1345 }
1346 if ( $inners->length > 0 ) {
1347 $s .= '>' . $this->expand( $inners->item( 0 ), $flags );
1348 if ( $closes->length > 0 ) {
1349 $s .= $this->expand( $closes->item( 0 ), $flags );
1350 }
1351 } else {
1352 $s .= '/>';
1353 }
1354 $out .= $s;
1355 } else {
1356 $params = [
1357 'name' => new PPNode_DOM( $names->item( 0 ) ),
1358 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null,
1359 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null,
1360 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null,
1361 ];
1362 $out .= $this->parser->extensionSubstitution( $params, $this );
1363 }
1364 } elseif ( $contextNode->nodeName == 'h' ) {
1365 # Heading
1366 $s = $this->expand( $contextNode->childNodes, $flags );
1367
1368 # Insert a heading marker only for <h> children of <root>
1369 # This is to stop extractSections from going over multiple tree levels
1370 if ( $contextNode->parentNode->nodeName == 'root' && $this->parser->ot['html'] ) {
1371 # Insert heading index marker
1372 $headingIndex = $contextNode->getAttribute( 'i' );
1373 $titleText = $this->title->getPrefixedDBkey();
1374 $this->parser->mHeadings[] = [ $titleText, $headingIndex ];
1375 $serial = count( $this->parser->mHeadings ) - 1;
1376 $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
1377 $count = $contextNode->getAttribute( 'level' );
1378 $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
1379 $this->parser->mStripState->addGeneral( $marker, '' );
1380 }
1381 $out .= $s;
1382 } else {
1383 # Generic recursive expansion
1384 $newIterator = $contextNode->childNodes;
1385 }
1386 } else {
1387 throw new MWException( __METHOD__ . ': Invalid parameter type' );
1388 }
1389
1390 if ( $newIterator !== false ) {
1391 if ( $newIterator instanceof PPNode_DOM ) {
1392 $newIterator = $newIterator->node;
1393 }
1394 $outStack[] = '';
1395 $iteratorStack[] = $newIterator;
1396 $indexStack[] = 0;
1397 } elseif ( $iteratorStack[$level] === false ) {
1398 // Return accumulated value to parent
1399 // With tail recursion
1400 while ( $iteratorStack[$level] === false && $level > 0 ) {
1401 $outStack[$level - 1] .= $out;
1402 array_pop( $outStack );
1403 array_pop( $iteratorStack );
1404 array_pop( $indexStack );
1405 $level--;
1406 }
1407 }
1408 }
1409 --$expansionDepth;
1410 return $outStack[0];
1411 }
1412
1419 public function implodeWithFlags( $sep, $flags /*, ... */ ) {
1420 $args = array_slice( func_get_args(), 2 );
1421
1422 $first = true;
1423 $s = '';
1424 foreach ( $args as $root ) {
1425 if ( $root instanceof PPNode_DOM ) {
1426 $root = $root->node;
1427 }
1428 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1429 $root = [ $root ];
1430 }
1431 foreach ( $root as $node ) {
1432 if ( $first ) {
1433 $first = false;
1434 } else {
1435 $s .= $sep;
1436 }
1437 $s .= $this->expand( $node, $flags );
1438 }
1439 }
1440 return $s;
1441 }
1442
1451 public function implode( $sep /*, ... */ ) {
1452 $args = array_slice( func_get_args(), 1 );
1453
1454 $first = true;
1455 $s = '';
1456 foreach ( $args as $root ) {
1457 if ( $root instanceof PPNode_DOM ) {
1458 $root = $root->node;
1459 }
1460 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1461 $root = [ $root ];
1462 }
1463 foreach ( $root as $node ) {
1464 if ( $first ) {
1465 $first = false;
1466 } else {
1467 $s .= $sep;
1468 }
1469 $s .= $this->expand( $node );
1470 }
1471 }
1472 return $s;
1473 }
1474
1483 public function virtualImplode( $sep /*, ... */ ) {
1484 $args = array_slice( func_get_args(), 1 );
1485 $out = [];
1486 $first = true;
1487
1488 foreach ( $args as $root ) {
1489 if ( $root instanceof PPNode_DOM ) {
1490 $root = $root->node;
1491 }
1492 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1493 $root = [ $root ];
1494 }
1495 foreach ( $root as $node ) {
1496 if ( $first ) {
1497 $first = false;
1498 } else {
1499 $out[] = $sep;
1500 }
1501 $out[] = $node;
1502 }
1503 }
1504 return $out;
1505 }
1506
1515 public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1516 $args = array_slice( func_get_args(), 3 );
1517 $out = [ $start ];
1518 $first = true;
1519
1520 foreach ( $args as $root ) {
1521 if ( $root instanceof PPNode_DOM ) {
1522 $root = $root->node;
1523 }
1524 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1525 $root = [ $root ];
1526 }
1527 foreach ( $root as $node ) {
1528 if ( $first ) {
1529 $first = false;
1530 } else {
1531 $out[] = $sep;
1532 }
1533 $out[] = $node;
1534 }
1535 }
1536 $out[] = $end;
1537 return $out;
1538 }
1539
1540 public function __toString() {
1541 return 'frame{}';
1542 }
1543
1544 public function getPDBK( $level = false ) {
1545 if ( $level === false ) {
1546 return $this->title->getPrefixedDBkey();
1547 } else {
1548 return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
1549 }
1550 }
1551
1555 public function getArguments() {
1556 return [];
1557 }
1558
1562 public function getNumberedArguments() {
1563 return [];
1564 }
1565
1569 public function getNamedArguments() {
1570 return [];
1571 }
1572
1578 public function isEmpty() {
1579 return true;
1580 }
1581
1586 public function getArgument( $name ) {
1587 return false;
1588 }
1589
1596 public function loopCheck( $title ) {
1597 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1598 }
1599
1605 public function isTemplate() {
1606 return false;
1607 }
1608
1614 public function getTitle() {
1615 return $this->title;
1616 }
1617
1623 public function setVolatile( $flag = true ) {
1624 $this->volatile = $flag;
1625 }
1626
1632 public function isVolatile() {
1633 return $this->volatile;
1634 }
1635
1641 public function setTTL( $ttl ) {
1642 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1643 $this->ttl = $ttl;
1644 }
1645 }
1646
1652 public function getTTL() {
1653 return $this->ttl;
1654 }
1655}
1656
1661// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1662class PPTemplateFrame_DOM extends PPFrame_DOM {
1663
1664 public $numberedArgs, $namedArgs;
1665
1669 public $parent;
1670 public $numberedExpansionCache, $namedExpansionCache;
1671
1679 public function __construct( $preprocessor, $parent = false, $numberedArgs = [],
1680 $namedArgs = [], $title = false
1681 ) {
1682 parent::__construct( $preprocessor );
1683
1684 $this->parent = $parent;
1685 $this->numberedArgs = $numberedArgs;
1686 $this->namedArgs = $namedArgs;
1687 $this->title = $title;
1688 $pdbk = $title ? $title->getPrefixedDBkey() : false;
1689 $this->titleCache = $parent->titleCache;
1690 $this->titleCache[] = $pdbk;
1691 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1692 if ( $pdbk !== false ) {
1693 $this->loopCheckHash[$pdbk] = true;
1694 }
1695 $this->depth = $parent->depth + 1;
1696 $this->numberedExpansionCache = $this->namedExpansionCache = [];
1697 }
1698
1699 public function __toString() {
1700 $s = 'tplframe{';
1701 $first = true;
1702 $args = $this->numberedArgs + $this->namedArgs;
1703 foreach ( $args as $name => $value ) {
1704 if ( $first ) {
1705 $first = false;
1706 } else {
1707 $s .= ', ';
1708 }
1709 $s .= "\"$name\":\"" .
1710 str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"';
1711 }
1712 $s .= '}';
1713 return $s;
1714 }
1715
1723 public function cachedExpand( $key, $root, $flags = 0 ) {
1724 if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1725 return $this->parent->childExpansionCache[$key];
1726 }
1727 $retval = $this->expand( $root, $flags );
1728 if ( !$this->isVolatile() ) {
1729 $this->parent->childExpansionCache[$key] = $retval;
1730 }
1731 return $retval;
1732 }
1733
1739 public function isEmpty() {
1740 return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1741 }
1742
1743 public function getArguments() {
1744 $arguments = [];
1745 foreach ( array_merge(
1746 array_keys( $this->numberedArgs ),
1747 array_keys( $this->namedArgs ) ) as $key ) {
1748 $arguments[$key] = $this->getArgument( $key );
1749 }
1750 return $arguments;
1751 }
1752
1753 public function getNumberedArguments() {
1754 $arguments = [];
1755 foreach ( array_keys( $this->numberedArgs ) as $key ) {
1756 $arguments[$key] = $this->getArgument( $key );
1757 }
1758 return $arguments;
1759 }
1760
1761 public function getNamedArguments() {
1762 $arguments = [];
1763 foreach ( array_keys( $this->namedArgs ) as $key ) {
1764 $arguments[$key] = $this->getArgument( $key );
1765 }
1766 return $arguments;
1767 }
1768
1773 public function getNumberedArgument( $index ) {
1774 if ( !isset( $this->numberedArgs[$index] ) ) {
1775 return false;
1776 }
1777 if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1778 # No trimming for unnamed arguments
1779 $this->numberedExpansionCache[$index] = $this->parent->expand(
1780 $this->numberedArgs[$index],
1781 PPFrame::STRIP_COMMENTS
1782 );
1783 }
1784 return $this->numberedExpansionCache[$index];
1785 }
1786
1791 public function getNamedArgument( $name ) {
1792 if ( !isset( $this->namedArgs[$name] ) ) {
1793 return false;
1794 }
1795 if ( !isset( $this->namedExpansionCache[$name] ) ) {
1796 # Trim named arguments post-expand, for backwards compatibility
1797 $this->namedExpansionCache[$name] = trim(
1798 $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1799 }
1800 return $this->namedExpansionCache[$name];
1801 }
1802
1807 public function getArgument( $name ) {
1808 $text = $this->getNumberedArgument( $name );
1809 if ( $text === false ) {
1810 $text = $this->getNamedArgument( $name );
1811 }
1812 return $text;
1813 }
1814
1820 public function isTemplate() {
1821 return true;
1822 }
1823
1824 public function setVolatile( $flag = true ) {
1825 parent::setVolatile( $flag );
1826 $this->parent->setVolatile( $flag );
1827 }
1828
1829 public function setTTL( $ttl ) {
1830 parent::setTTL( $ttl );
1831 $this->parent->setTTL( $ttl );
1832 }
1833}
1834
1839// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1840class PPCustomFrame_DOM extends PPFrame_DOM {
1841
1842 public $args;
1843
1844 public function __construct( $preprocessor, $args ) {
1845 parent::__construct( $preprocessor );
1846 $this->args = $args;
1847 }
1848
1849 public function __toString() {
1850 $s = 'cstmframe{';
1851 $first = true;
1852 foreach ( $this->args as $name => $value ) {
1853 if ( $first ) {
1854 $first = false;
1855 } else {
1856 $s .= ', ';
1857 }
1858 $s .= "\"$name\":\"" .
1859 str_replace( '"', '\\"', $value->__toString() ) . '"';
1860 }
1861 $s .= '}';
1862 return $s;
1863 }
1864
1868 public function isEmpty() {
1869 return !count( $this->args );
1870 }
1871
1876 public function getArgument( $index ) {
1877 if ( !isset( $this->args[$index] ) ) {
1878 return false;
1879 }
1880 return $this->args[$index];
1881 }
1882
1883 public function getArguments() {
1884 return $this->args;
1885 }
1886}
1887
1891// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1892class PPNode_DOM implements PPNode {
1893
1897 public $node;
1898 public $xpath;
1899
1900 public function __construct( $node, $xpath = false ) {
1901 $this->node = $node;
1902 }
1903
1907 public function getXPath() {
1908 if ( $this->xpath === null ) {
1909 $this->xpath = new DOMXPath( $this->node->ownerDocument );
1910 }
1911 return $this->xpath;
1912 }
1913
1914 public function __toString() {
1915 if ( $this->node instanceof DOMNodeList ) {
1916 $s = '';
1917 foreach ( $this->node as $node ) {
1918 $s .= $node->ownerDocument->saveXML( $node );
1919 }
1920 } else {
1921 $s = $this->node->ownerDocument->saveXML( $this->node );
1922 }
1923 return $s;
1924 }
1925
1929 public function getChildren() {
1930 return $this->node->childNodes ? new self( $this->node->childNodes ) : false;
1931 }
1932
1936 public function getFirstChild() {
1937 return $this->node->firstChild ? new self( $this->node->firstChild ) : false;
1938 }
1939
1943 public function getNextSibling() {
1944 return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false;
1945 }
1946
1952 public function getChildrenOfType( $type ) {
1953 return new self( $this->getXPath()->query( $type, $this->node ) );
1954 }
1955
1959 public function getLength() {
1960 if ( $this->node instanceof DOMNodeList ) {
1961 return $this->node->length;
1962 } else {
1963 return false;
1964 }
1965 }
1966
1971 public function item( $i ) {
1972 $item = $this->node->item( $i );
1973 return $item ? new self( $item ) : false;
1974 }
1975
1979 public function getName() {
1980 if ( $this->node instanceof DOMNodeList ) {
1981 return '#nodelist';
1982 } else {
1983 return $this->node->nodeName;
1984 }
1985 }
1986
1996 public function splitArg() {
1997 $xpath = $this->getXPath();
1998 $names = $xpath->query( 'name', $this->node );
1999 $values = $xpath->query( 'value', $this->node );
2000 if ( !$names->length || !$values->length ) {
2001 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
2002 }
2003 $name = $names->item( 0 );
2004 $index = $name->getAttribute( 'index' );
2005 return [
2006 'name' => new self( $name ),
2007 'index' => $index,
2008 'value' => new self( $values->item( 0 ) ) ];
2009 }
2010
2018 public function splitExt() {
2019 $xpath = $this->getXPath();
2020 $names = $xpath->query( 'name', $this->node );
2021 $attrs = $xpath->query( 'attr', $this->node );
2022 $inners = $xpath->query( 'inner', $this->node );
2023 $closes = $xpath->query( 'close', $this->node );
2024 if ( !$names->length || !$attrs->length ) {
2025 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
2026 }
2027 $parts = [
2028 'name' => new self( $names->item( 0 ) ),
2029 'attr' => new self( $attrs->item( 0 ) ) ];
2030 if ( $inners->length ) {
2031 $parts['inner'] = new self( $inners->item( 0 ) );
2032 }
2033 if ( $closes->length ) {
2034 $parts['close'] = new self( $closes->item( 0 ) );
2035 }
2036 return $parts;
2037 }
2038
2044 public function splitHeading() {
2045 if ( $this->getName() !== 'h' ) {
2046 throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
2047 }
2048 return [
2049 'i' => $this->node->getAttribute( 'i' ),
2050 'level' => $this->node->getAttribute( 'level' ),
2051 'contents' => $this->getChildren()
2052 ];
2053 }
2054}
$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()
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:1421
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
> value, names are case insensitive). Two headers get special handling:If-Modified-Since(value must be a valid HTTP date) and Range(must be of the form "bytes=(\d*-\d*)") will be honored when streaming the file. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED! Use $magicWords in a file listed in $wgExtensionMessagesFiles instead. Use this to define synonyms of magic words depending of the language & $magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED! Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead. Use to define aliases of special pages names depending of the language & $specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) name
Definition hooks.txt:1840
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 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