MediaWiki REL1_32
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 // FIXME: Don't use assert()
570 // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.assert
571 assert( $piece->open === "\n" );
572 $part = $piece->getCurrentPart();
573 // Search back through the input to see if it has a proper close.
574 // Do this using the reversed string since the other solutions
575 // (end anchor, etc.) are inefficient.
576 $wsLength = strspn( $revText, " \t", $lengthText - $i );
577 $searchStart = $i - $wsLength;
578 if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
579 // Comment found at line end
580 // Search for equals signs before the comment
581 $searchStart = $part->visualEnd;
582 $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
583 }
584 $count = $piece->count;
585 $equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
586 if ( $equalsLength > 0 ) {
587 if ( $searchStart - $equalsLength == $piece->startPos ) {
588 // This is just a single string of equals signs on its own line
589 // Replicate the doHeadings behavior /={count}(.+)={count}/
590 // First find out how many equals signs there really are (don't stop at 6)
591 $count = $equalsLength;
592 if ( $count < 3 ) {
593 $count = 0;
594 } else {
595 $count = min( 6, intval( ( $count - 1 ) / 2 ) );
596 }
597 } else {
598 $count = min( $equalsLength, $count );
599 }
600 if ( $count > 0 ) {
601 // Normal match, output <h>
602 $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>";
603 $headingIndex++;
604 } else {
605 // Single equals sign on its own line, count=0
606 $element = $accum;
607 }
608 } else {
609 // No match, no <h>, just pass down the inner text
610 $element = $accum;
611 }
612 // Unwind the stack
613 $stack->pop();
614 $accum =& $stack->getAccum();
615 $stackFlags = $stack->getFlags();
616 if ( isset( $stackFlags['findEquals'] ) ) {
617 $findEquals = $stackFlags['findEquals'];
618 }
619 if ( isset( $stackFlags['findPipe'] ) ) {
620 $findPipe = $stackFlags['findPipe'];
621 }
622 if ( isset( $stackFlags['inHeading'] ) ) {
623 $inHeading = $stackFlags['inHeading'];
624 }
625
626 // Append the result to the enclosing accumulator
627 $accum .= $element;
628 // Note that we do NOT increment the input pointer.
629 // This is because the closing linebreak could be the opening linebreak of
630 // another heading. Infinite loops are avoided because the next iteration MUST
631 // hit the heading open case above, which unconditionally increments the
632 // input pointer.
633 } elseif ( $found == 'open' ) {
634 # count opening brace characters
635 $curLen = strlen( $curChar );
636 $count = ( $curLen > 1 ) ?
637 # allow the final character to repeat
638 strspn( $text, $curChar[$curLen - 1], $i + 1 ) + 1 :
639 strspn( $text, $curChar, $i );
640
641 $savedPrefix = '';
642 $lineStart = ( $i > 0 && $text[$i - 1] == "\n" );
643
644 if ( $curChar === "-{" && $count > $curLen ) {
645 // -{ => {{ transition because rightmost wins
646 $savedPrefix = '-';
647 $i++;
648 $curChar = '{';
649 $count--;
650 $rule = $this->rules[$curChar];
651 }
652
653 # we need to add to stack only if opening brace count is enough for one of the rules
654 if ( $count >= $rule['min'] ) {
655 # Add it to the stack
656 $piece = [
657 'open' => $curChar,
658 'close' => $rule['end'],
659 'savedPrefix' => $savedPrefix,
660 'count' => $count,
661 'lineStart' => $lineStart,
662 ];
663
664 $stack->push( $piece );
665 $accum =& $stack->getAccum();
666 $stackFlags = $stack->getFlags();
667 if ( isset( $stackFlags['findEquals'] ) ) {
668 $findEquals = $stackFlags['findEquals'];
669 }
670 if ( isset( $stackFlags['findPipe'] ) ) {
671 $findPipe = $stackFlags['findPipe'];
672 }
673 if ( isset( $stackFlags['inHeading'] ) ) {
674 $inHeading = $stackFlags['inHeading'];
675 }
676 } else {
677 # Add literal brace(s)
678 $accum .= htmlspecialchars( $savedPrefix . str_repeat( $curChar, $count ) );
679 }
680 $i += $count;
681 } elseif ( $found == 'close' ) {
682 $piece = $stack->top;
683 # lets check if there are enough characters for closing brace
684 $maxCount = $piece->count;
685 if ( $piece->close === '}-' && $curChar === '}' ) {
686 $maxCount--; # don't try to match closing '-' as a '}'
687 }
688 $curLen = strlen( $curChar );
689 $count = ( $curLen > 1 ) ? $curLen :
690 strspn( $text, $curChar, $i, $maxCount );
691
692 # check for maximum matching characters (if there are 5 closing
693 # characters, we will probably need only 3 - depending on the rules)
694 $rule = $this->rules[$piece->open];
695 if ( $count > $rule['max'] ) {
696 # The specified maximum exists in the callback array, unless the caller
697 # has made an error
698 $matchingCount = $rule['max'];
699 } else {
700 # Count is less than the maximum
701 # Skip any gaps in the callback array to find the true largest match
702 # Need to use array_key_exists not isset because the callback can be null
703 $matchingCount = $count;
704 while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
705 --$matchingCount;
706 }
707 }
708
709 if ( $matchingCount <= 0 ) {
710 # No matching element found in callback array
711 # Output a literal closing brace and continue
712 $endText = substr( $text, $i, $count );
713 $accum .= htmlspecialchars( $endText );
714 $i += $count;
715 continue;
716 }
717 $name = $rule['names'][$matchingCount];
718 if ( $name === null ) {
719 // No element, just literal text
720 $endText = substr( $text, $i, $matchingCount );
721 $element = $piece->breakSyntax( $matchingCount ) . $endText;
722 } else {
723 # Create XML element
724 # Note: $parts is already XML, does not need to be encoded further
725 $parts = $piece->parts;
726 $title = $parts[0]->out;
727 unset( $parts[0] );
728
729 # The invocation is at the start of the line if lineStart is set in
730 # the stack, and all opening brackets are used up.
731 if ( $maxCount == $matchingCount &&
732 !empty( $piece->lineStart ) &&
733 strlen( $piece->savedPrefix ) == 0 ) {
734 $attr = ' lineStart="1"';
735 } else {
736 $attr = '';
737 }
738
739 $element = "<$name$attr>";
740 $element .= "<title>$title</title>";
741 $argIndex = 1;
742 foreach ( $parts as $part ) {
743 if ( isset( $part->eqpos ) ) {
744 $argName = substr( $part->out, 0, $part->eqpos );
745 $argValue = substr( $part->out, $part->eqpos + 1 );
746 $element .= "<part><name>$argName</name>=<value>$argValue</value></part>";
747 } else {
748 $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>";
749 $argIndex++;
750 }
751 }
752 $element .= "</$name>";
753 }
754
755 # Advance input pointer
756 $i += $matchingCount;
757
758 # Unwind the stack
759 $stack->pop();
760 $accum =& $stack->getAccum();
761
762 # Re-add the old stack element if it still has unmatched opening characters remaining
763 if ( $matchingCount < $piece->count ) {
764 $piece->parts = [ new PPDPart ];
765 $piece->count -= $matchingCount;
766 # do we still qualify for any callback with remaining count?
767 $min = $this->rules[$piece->open]['min'];
768 if ( $piece->count >= $min ) {
769 $stack->push( $piece );
770 $accum =& $stack->getAccum();
771 } elseif ( $piece->count == 1 && $piece->open === '{' && $piece->savedPrefix === '-' ) {
772 $piece->savedPrefix = '';
773 $piece->open = '-{';
774 $piece->count = 2;
775 $piece->close = $this->rules[$piece->open]['end'];
776 $stack->push( $piece );
777 $accum =& $stack->getAccum();
778 } else {
779 $s = substr( $piece->open, 0, -1 );
780 $s .= str_repeat(
781 substr( $piece->open, -1 ),
782 $piece->count - strlen( $s )
783 );
784 $accum .= $piece->savedPrefix . $s;
785 }
786 } elseif ( $piece->savedPrefix !== '' ) {
787 $accum .= $piece->savedPrefix;
788 }
789
790 $stackFlags = $stack->getFlags();
791 if ( isset( $stackFlags['findEquals'] ) ) {
792 $findEquals = $stackFlags['findEquals'];
793 }
794 if ( isset( $stackFlags['findPipe'] ) ) {
795 $findPipe = $stackFlags['findPipe'];
796 }
797 if ( isset( $stackFlags['inHeading'] ) ) {
798 $inHeading = $stackFlags['inHeading'];
799 }
800
801 # Add XML element to the enclosing accumulator
802 $accum .= $element;
803 } elseif ( $found == 'pipe' ) {
804 $findEquals = true; // shortcut for getFlags()
805 $stack->addPart();
806 $accum =& $stack->getAccum();
807 ++$i;
808 } elseif ( $found == 'equals' ) {
809 $findEquals = false; // shortcut for getFlags()
810 $stack->getCurrentPart()->eqpos = strlen( $accum );
811 $accum .= '=';
812 ++$i;
813 }
814 }
815
816 # Output any remaining unclosed brackets
817 foreach ( $stack->stack as $piece ) {
818 $stack->rootAccum .= $piece->breakSyntax();
819 }
820 $stack->rootAccum .= '</root>';
821 $xml = $stack->rootAccum;
822
823 return $xml;
824 }
825}
826
831class PPDStack {
832 public $stack, $rootAccum;
833
837 public $top;
838 public $out;
839 public $elementClass = PPDStackElement::class;
840
841 public static $false = false;
842
843 public function __construct() {
844 $this->stack = [];
845 $this->top = false;
846 $this->rootAccum = '';
847 $this->accum =& $this->rootAccum;
848 }
849
853 public function count() {
854 return count( $this->stack );
855 }
856
857 public function &getAccum() {
858 return $this->accum;
859 }
860
861 public function getCurrentPart() {
862 if ( $this->top === false ) {
863 return false;
864 } else {
865 return $this->top->getCurrentPart();
866 }
867 }
868
869 public function push( $data ) {
870 if ( $data instanceof $this->elementClass ) {
871 $this->stack[] = $data;
872 } else {
873 $class = $this->elementClass;
874 $this->stack[] = new $class( $data );
875 }
876 $this->top = $this->stack[count( $this->stack ) - 1];
877 $this->accum =& $this->top->getAccum();
878 }
879
880 public function pop() {
881 if ( !count( $this->stack ) ) {
882 throw new MWException( __METHOD__ . ': no elements remaining' );
883 }
884 $temp = array_pop( $this->stack );
885
886 if ( count( $this->stack ) ) {
887 $this->top = $this->stack[count( $this->stack ) - 1];
888 $this->accum =& $this->top->getAccum();
889 } else {
890 $this->top = self::$false;
891 $this->accum =& $this->rootAccum;
892 }
893 return $temp;
894 }
895
896 public function addPart( $s = '' ) {
897 $this->top->addPart( $s );
898 $this->accum =& $this->top->getAccum();
899 }
900
904 public function getFlags() {
905 if ( !count( $this->stack ) ) {
906 return [
907 'findEquals' => false,
908 'findPipe' => false,
909 'inHeading' => false,
910 ];
911 } else {
912 return $this->top->getFlags();
913 }
914 }
915}
916
920class PPDStackElement {
924 public $open;
925
929 public $close;
930
935 public $savedPrefix = '';
936
940 public $count;
941
945 public $parts;
946
951 public $lineStart;
952
953 public $partClass = PPDPart::class;
954
955 public function __construct( $data = [] ) {
956 $class = $this->partClass;
957 $this->parts = [ new $class ];
958
959 foreach ( $data as $name => $value ) {
960 $this->$name = $value;
961 }
962 }
963
964 public function &getAccum() {
965 return $this->parts[count( $this->parts ) - 1]->out;
966 }
967
968 public function addPart( $s = '' ) {
969 $class = $this->partClass;
970 $this->parts[] = new $class( $s );
971 }
972
973 public function getCurrentPart() {
974 return $this->parts[count( $this->parts ) - 1];
975 }
976
980 public function getFlags() {
981 $partCount = count( $this->parts );
982 $findPipe = $this->open != "\n" && $this->open != '[';
983 return [
984 'findPipe' => $findPipe,
985 'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ),
986 'inHeading' => $this->open == "\n",
987 ];
988 }
989
996 public function breakSyntax( $openingCount = false ) {
997 if ( $this->open == "\n" ) {
998 $s = $this->savedPrefix . $this->parts[0]->out;
999 } else {
1000 if ( $openingCount === false ) {
1001 $openingCount = $this->count;
1002 }
1003 $s = substr( $this->open, 0, -1 );
1004 $s .= str_repeat(
1005 substr( $this->open, -1 ),
1006 $openingCount - strlen( $s )
1007 );
1008 $s = $this->savedPrefix . $s;
1009 $first = true;
1010 foreach ( $this->parts as $part ) {
1011 if ( $first ) {
1012 $first = false;
1013 } else {
1014 $s .= '|';
1015 }
1016 $s .= $part->out;
1017 }
1018 }
1019 return $s;
1020 }
1021}
1022
1026class PPDPart {
1030 public $out;
1031
1032 // Optional member variables:
1033 // eqpos Position of equals sign in output accumulator
1034 // commentEnd Past-the-end input pointer for the last comment encountered
1035 // visualEnd Past-the-end input pointer for the end of the accumulator minus comments
1036
1037 public function __construct( $out = '' ) {
1038 $this->out = $out;
1039 }
1040}
1041
1046// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1047class PPFrame_DOM implements PPFrame {
1048
1052 public $preprocessor;
1053
1057 public $parser;
1058
1062 public $title;
1063 public $titleCache;
1064
1069 public $loopCheckHash;
1070
1075 public $depth;
1076
1077 private $volatile = false;
1078 private $ttl = null;
1079
1083 protected $childExpansionCache;
1084
1089 public function __construct( $preprocessor ) {
1090 $this->preprocessor = $preprocessor;
1091 $this->parser = $preprocessor->parser;
1092 $this->title = $this->parser->mTitle;
1093 $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
1094 $this->loopCheckHash = [];
1095 $this->depth = 0;
1096 $this->childExpansionCache = [];
1097 }
1098
1108 public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
1109 $namedArgs = [];
1110 $numberedArgs = [];
1111 if ( $title === false ) {
1112 $title = $this->title;
1113 }
1114 if ( $args !== false ) {
1115 $xpath = false;
1116 if ( $args instanceof PPNode ) {
1117 $args = $args->node;
1118 }
1119 foreach ( $args as $arg ) {
1120 if ( $arg instanceof PPNode ) {
1121 $arg = $arg->node;
1122 }
1123 if ( !$xpath || $xpath->document !== $arg->ownerDocument ) {
1124 $xpath = new DOMXPath( $arg->ownerDocument );
1125 }
1126
1127 $nameNodes = $xpath->query( 'name', $arg );
1128 $value = $xpath->query( 'value', $arg );
1129 if ( $nameNodes->item( 0 )->hasAttributes() ) {
1130 // Numbered parameter
1131 $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
1132 $index = $index - $indexOffset;
1133 if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
1134 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
1135 wfEscapeWikiText( $this->title ),
1136 wfEscapeWikiText( $title ),
1137 wfEscapeWikiText( $index ) )->text() );
1138 $this->parser->addTrackingCategory( 'duplicate-args-category' );
1139 }
1140 $numberedArgs[$index] = $value->item( 0 );
1141 unset( $namedArgs[$index] );
1142 } else {
1143 // Named parameter
1144 $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) );
1145 if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
1146 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
1147 wfEscapeWikiText( $this->title ),
1148 wfEscapeWikiText( $title ),
1149 wfEscapeWikiText( $name ) )->text() );
1150 $this->parser->addTrackingCategory( 'duplicate-args-category' );
1151 }
1152 $namedArgs[$name] = $value->item( 0 );
1153 unset( $numberedArgs[$name] );
1154 }
1155 }
1156 }
1157 return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
1158 }
1159
1167 public function cachedExpand( $key, $root, $flags = 0 ) {
1168 // we don't have a parent, so we don't have a cache
1169 return $this->expand( $root, $flags );
1170 }
1171
1178 public function expand( $root, $flags = 0 ) {
1179 static $expansionDepth = 0;
1180 if ( is_string( $root ) ) {
1181 return $root;
1182 }
1183
1184 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
1185 $this->parser->limitationWarn( 'node-count-exceeded',
1186 $this->parser->mPPNodeCount,
1187 $this->parser->mOptions->getMaxPPNodeCount()
1188 );
1189 return '<span class="error">Node-count limit exceeded</span>';
1190 }
1191
1192 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
1193 $this->parser->limitationWarn( 'expansion-depth-exceeded',
1194 $expansionDepth,
1195 $this->parser->mOptions->getMaxPPExpandDepth()
1196 );
1197 return '<span class="error">Expansion depth limit exceeded</span>';
1198 }
1199 ++$expansionDepth;
1200 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
1201 $this->parser->mHighestExpansionDepth = $expansionDepth;
1202 }
1203
1204 if ( $root instanceof PPNode_DOM ) {
1205 $root = $root->node;
1206 }
1207 if ( $root instanceof DOMDocument ) {
1208 $root = $root->documentElement;
1209 }
1210
1211 $outStack = [ '', '' ];
1212 $iteratorStack = [ false, $root ];
1213 $indexStack = [ 0, 0 ];
1214
1215 while ( count( $iteratorStack ) > 1 ) {
1216 $level = count( $outStack ) - 1;
1217 $iteratorNode =& $iteratorStack[$level];
1218 $out =& $outStack[$level];
1219 $index =& $indexStack[$level];
1220
1221 if ( $iteratorNode instanceof PPNode_DOM ) {
1222 $iteratorNode = $iteratorNode->node;
1223 }
1224
1225 if ( is_array( $iteratorNode ) ) {
1226 if ( $index >= count( $iteratorNode ) ) {
1227 // All done with this iterator
1228 $iteratorStack[$level] = false;
1229 $contextNode = false;
1230 } else {
1231 $contextNode = $iteratorNode[$index];
1232 $index++;
1233 }
1234 } elseif ( $iteratorNode instanceof DOMNodeList ) {
1235 if ( $index >= $iteratorNode->length ) {
1236 // All done with this iterator
1237 $iteratorStack[$level] = false;
1238 $contextNode = false;
1239 } else {
1240 $contextNode = $iteratorNode->item( $index );
1241 $index++;
1242 }
1243 } else {
1244 // Copy to $contextNode and then delete from iterator stack,
1245 // because this is not an iterator but we do have to execute it once
1246 $contextNode = $iteratorStack[$level];
1247 $iteratorStack[$level] = false;
1248 }
1249
1250 if ( $contextNode instanceof PPNode_DOM ) {
1251 $contextNode = $contextNode->node;
1252 }
1253
1254 $newIterator = false;
1255
1256 if ( $contextNode === false ) {
1257 // nothing to do
1258 } elseif ( is_string( $contextNode ) ) {
1259 $out .= $contextNode;
1260 } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) {
1261 $newIterator = $contextNode;
1262 } elseif ( $contextNode instanceof DOMNode ) {
1263 if ( $contextNode->nodeType == XML_TEXT_NODE ) {
1264 $out .= $contextNode->nodeValue;
1265 } elseif ( $contextNode->nodeName == 'template' ) {
1266 # Double-brace expansion
1267 $xpath = new DOMXPath( $contextNode->ownerDocument );
1268 $titles = $xpath->query( 'title', $contextNode );
1269 $title = $titles->item( 0 );
1270 $parts = $xpath->query( 'part', $contextNode );
1271 if ( $flags & PPFrame::NO_TEMPLATES ) {
1272 $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
1273 } else {
1274 $lineStart = $contextNode->getAttribute( 'lineStart' );
1275 $params = [
1276 'title' => new PPNode_DOM( $title ),
1277 'parts' => new PPNode_DOM( $parts ),
1278 'lineStart' => $lineStart ];
1279 $ret = $this->parser->braceSubstitution( $params, $this );
1280 if ( isset( $ret['object'] ) ) {
1281 $newIterator = $ret['object'];
1282 } else {
1283 $out .= $ret['text'];
1284 }
1285 }
1286 } elseif ( $contextNode->nodeName == 'tplarg' ) {
1287 # Triple-brace expansion
1288 $xpath = new DOMXPath( $contextNode->ownerDocument );
1289 $titles = $xpath->query( 'title', $contextNode );
1290 $title = $titles->item( 0 );
1291 $parts = $xpath->query( 'part', $contextNode );
1292 if ( $flags & PPFrame::NO_ARGS ) {
1293 $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
1294 } else {
1295 $params = [
1296 'title' => new PPNode_DOM( $title ),
1297 'parts' => new PPNode_DOM( $parts ) ];
1298 $ret = $this->parser->argSubstitution( $params, $this );
1299 if ( isset( $ret['object'] ) ) {
1300 $newIterator = $ret['object'];
1301 } else {
1302 $out .= $ret['text'];
1303 }
1304 }
1305 } elseif ( $contextNode->nodeName == 'comment' ) {
1306 # HTML-style comment
1307 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1308 # Not in RECOVER_COMMENTS mode (msgnw) though.
1309 if ( ( $this->parser->ot['html']
1310 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1311 || ( $flags & PPFrame::STRIP_COMMENTS )
1312 ) && !( $flags & PPFrame::RECOVER_COMMENTS )
1313 ) {
1314 $out .= '';
1315 } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1316 # Add a strip marker in PST mode so that pstPass2() can
1317 # run some old-fashioned regexes on the result.
1318 # Not in RECOVER_COMMENTS mode (extractSections) though.
1319 $out .= $this->parser->insertStripItem( $contextNode->textContent );
1320 } else {
1321 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1322 $out .= $contextNode->textContent;
1323 }
1324 } elseif ( $contextNode->nodeName == 'ignore' ) {
1325 # Output suppression used by <includeonly> etc.
1326 # OT_WIKI will only respect <ignore> in substed templates.
1327 # The other output types respect it unless NO_IGNORE is set.
1328 # extractSections() sets NO_IGNORE and so never respects it.
1329 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1330 || ( $flags & PPFrame::NO_IGNORE )
1331 ) {
1332 $out .= $contextNode->textContent;
1333 } else {
1334 $out .= '';
1335 }
1336 } elseif ( $contextNode->nodeName == 'ext' ) {
1337 # Extension tag
1338 $xpath = new DOMXPath( $contextNode->ownerDocument );
1339 $names = $xpath->query( 'name', $contextNode );
1340 $attrs = $xpath->query( 'attr', $contextNode );
1341 $inners = $xpath->query( 'inner', $contextNode );
1342 $closes = $xpath->query( 'close', $contextNode );
1343 if ( $flags & PPFrame::NO_TAGS ) {
1344 $s = '<' . $this->expand( $names->item( 0 ), $flags );
1345 if ( $attrs->length > 0 ) {
1346 $s .= $this->expand( $attrs->item( 0 ), $flags );
1347 }
1348 if ( $inners->length > 0 ) {
1349 $s .= '>' . $this->expand( $inners->item( 0 ), $flags );
1350 if ( $closes->length > 0 ) {
1351 $s .= $this->expand( $closes->item( 0 ), $flags );
1352 }
1353 } else {
1354 $s .= '/>';
1355 }
1356 $out .= $s;
1357 } else {
1358 $params = [
1359 'name' => new PPNode_DOM( $names->item( 0 ) ),
1360 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null,
1361 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null,
1362 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null,
1363 ];
1364 $out .= $this->parser->extensionSubstitution( $params, $this );
1365 }
1366 } elseif ( $contextNode->nodeName == 'h' ) {
1367 # Heading
1368 $s = $this->expand( $contextNode->childNodes, $flags );
1369
1370 # Insert a heading marker only for <h> children of <root>
1371 # This is to stop extractSections from going over multiple tree levels
1372 if ( $contextNode->parentNode->nodeName == 'root' && $this->parser->ot['html'] ) {
1373 # Insert heading index marker
1374 $headingIndex = $contextNode->getAttribute( 'i' );
1375 $titleText = $this->title->getPrefixedDBkey();
1376 $this->parser->mHeadings[] = [ $titleText, $headingIndex ];
1377 $serial = count( $this->parser->mHeadings ) - 1;
1378 $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
1379 $count = $contextNode->getAttribute( 'level' );
1380 $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
1381 $this->parser->mStripState->addGeneral( $marker, '' );
1382 }
1383 $out .= $s;
1384 } else {
1385 # Generic recursive expansion
1386 $newIterator = $contextNode->childNodes;
1387 }
1388 } else {
1389 throw new MWException( __METHOD__ . ': Invalid parameter type' );
1390 }
1391
1392 if ( $newIterator !== false ) {
1393 if ( $newIterator instanceof PPNode_DOM ) {
1394 $newIterator = $newIterator->node;
1395 }
1396 $outStack[] = '';
1397 $iteratorStack[] = $newIterator;
1398 $indexStack[] = 0;
1399 } elseif ( $iteratorStack[$level] === false ) {
1400 // Return accumulated value to parent
1401 // With tail recursion
1402 while ( $iteratorStack[$level] === false && $level > 0 ) {
1403 $outStack[$level - 1] .= $out;
1404 array_pop( $outStack );
1405 array_pop( $iteratorStack );
1406 array_pop( $indexStack );
1407 $level--;
1408 }
1409 }
1410 }
1411 --$expansionDepth;
1412 return $outStack[0];
1413 }
1414
1421 public function implodeWithFlags( $sep, $flags /*, ... */ ) {
1422 $args = array_slice( func_get_args(), 2 );
1423
1424 $first = true;
1425 $s = '';
1426 foreach ( $args as $root ) {
1427 if ( $root instanceof PPNode_DOM ) {
1428 $root = $root->node;
1429 }
1430 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1431 $root = [ $root ];
1432 }
1433 foreach ( $root as $node ) {
1434 if ( $first ) {
1435 $first = false;
1436 } else {
1437 $s .= $sep;
1438 }
1439 $s .= $this->expand( $node, $flags );
1440 }
1441 }
1442 return $s;
1443 }
1444
1453 public function implode( $sep /*, ... */ ) {
1454 $args = array_slice( func_get_args(), 1 );
1455
1456 $first = true;
1457 $s = '';
1458 foreach ( $args as $root ) {
1459 if ( $root instanceof PPNode_DOM ) {
1460 $root = $root->node;
1461 }
1462 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1463 $root = [ $root ];
1464 }
1465 foreach ( $root as $node ) {
1466 if ( $first ) {
1467 $first = false;
1468 } else {
1469 $s .= $sep;
1470 }
1471 $s .= $this->expand( $node );
1472 }
1473 }
1474 return $s;
1475 }
1476
1485 public function virtualImplode( $sep /*, ... */ ) {
1486 $args = array_slice( func_get_args(), 1 );
1487 $out = [];
1488 $first = true;
1489
1490 foreach ( $args as $root ) {
1491 if ( $root instanceof PPNode_DOM ) {
1492 $root = $root->node;
1493 }
1494 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1495 $root = [ $root ];
1496 }
1497 foreach ( $root as $node ) {
1498 if ( $first ) {
1499 $first = false;
1500 } else {
1501 $out[] = $sep;
1502 }
1503 $out[] = $node;
1504 }
1505 }
1506 return $out;
1507 }
1508
1517 public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1518 $args = array_slice( func_get_args(), 3 );
1519 $out = [ $start ];
1520 $first = true;
1521
1522 foreach ( $args as $root ) {
1523 if ( $root instanceof PPNode_DOM ) {
1524 $root = $root->node;
1525 }
1526 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1527 $root = [ $root ];
1528 }
1529 foreach ( $root as $node ) {
1530 if ( $first ) {
1531 $first = false;
1532 } else {
1533 $out[] = $sep;
1534 }
1535 $out[] = $node;
1536 }
1537 }
1538 $out[] = $end;
1539 return $out;
1540 }
1541
1542 public function __toString() {
1543 return 'frame{}';
1544 }
1545
1546 public function getPDBK( $level = false ) {
1547 if ( $level === false ) {
1548 return $this->title->getPrefixedDBkey();
1549 } else {
1550 return $this->titleCache[$level] ?? false;
1551 }
1552 }
1553
1557 public function getArguments() {
1558 return [];
1559 }
1560
1564 public function getNumberedArguments() {
1565 return [];
1566 }
1567
1571 public function getNamedArguments() {
1572 return [];
1573 }
1574
1580 public function isEmpty() {
1581 return true;
1582 }
1583
1588 public function getArgument( $name ) {
1589 return false;
1590 }
1591
1598 public function loopCheck( $title ) {
1599 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1600 }
1601
1607 public function isTemplate() {
1608 return false;
1609 }
1610
1616 public function getTitle() {
1617 return $this->title;
1618 }
1619
1625 public function setVolatile( $flag = true ) {
1626 $this->volatile = $flag;
1627 }
1628
1634 public function isVolatile() {
1635 return $this->volatile;
1636 }
1637
1643 public function setTTL( $ttl ) {
1644 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1645 $this->ttl = $ttl;
1646 }
1647 }
1648
1654 public function getTTL() {
1655 return $this->ttl;
1656 }
1657}
1658
1663// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1664class PPTemplateFrame_DOM extends PPFrame_DOM {
1665
1666 public $numberedArgs, $namedArgs;
1667
1671 public $parent;
1672 public $numberedExpansionCache, $namedExpansionCache;
1673
1681 public function __construct( $preprocessor, $parent = false, $numberedArgs = [],
1682 $namedArgs = [], $title = false
1683 ) {
1684 parent::__construct( $preprocessor );
1685
1686 $this->parent = $parent;
1687 $this->numberedArgs = $numberedArgs;
1688 $this->namedArgs = $namedArgs;
1689 $this->title = $title;
1690 $pdbk = $title ? $title->getPrefixedDBkey() : false;
1691 $this->titleCache = $parent->titleCache;
1692 $this->titleCache[] = $pdbk;
1693 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1694 if ( $pdbk !== false ) {
1695 $this->loopCheckHash[$pdbk] = true;
1696 }
1697 $this->depth = $parent->depth + 1;
1698 $this->numberedExpansionCache = $this->namedExpansionCache = [];
1699 }
1700
1701 public function __toString() {
1702 $s = 'tplframe{';
1703 $first = true;
1704 $args = $this->numberedArgs + $this->namedArgs;
1705 foreach ( $args as $name => $value ) {
1706 if ( $first ) {
1707 $first = false;
1708 } else {
1709 $s .= ', ';
1710 }
1711 $s .= "\"$name\":\"" .
1712 str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"';
1713 }
1714 $s .= '}';
1715 return $s;
1716 }
1717
1725 public function cachedExpand( $key, $root, $flags = 0 ) {
1726 if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1727 return $this->parent->childExpansionCache[$key];
1728 }
1729 $retval = $this->expand( $root, $flags );
1730 if ( !$this->isVolatile() ) {
1731 $this->parent->childExpansionCache[$key] = $retval;
1732 }
1733 return $retval;
1734 }
1735
1741 public function isEmpty() {
1742 return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1743 }
1744
1745 public function getArguments() {
1746 $arguments = [];
1747 foreach ( array_merge(
1748 array_keys( $this->numberedArgs ),
1749 array_keys( $this->namedArgs ) ) as $key ) {
1750 $arguments[$key] = $this->getArgument( $key );
1751 }
1752 return $arguments;
1753 }
1754
1755 public function getNumberedArguments() {
1756 $arguments = [];
1757 foreach ( array_keys( $this->numberedArgs ) as $key ) {
1758 $arguments[$key] = $this->getArgument( $key );
1759 }
1760 return $arguments;
1761 }
1762
1763 public function getNamedArguments() {
1764 $arguments = [];
1765 foreach ( array_keys( $this->namedArgs ) as $key ) {
1766 $arguments[$key] = $this->getArgument( $key );
1767 }
1768 return $arguments;
1769 }
1770
1775 public function getNumberedArgument( $index ) {
1776 if ( !isset( $this->numberedArgs[$index] ) ) {
1777 return false;
1778 }
1779 if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1780 # No trimming for unnamed arguments
1781 $this->numberedExpansionCache[$index] = $this->parent->expand(
1782 $this->numberedArgs[$index],
1783 PPFrame::STRIP_COMMENTS
1784 );
1785 }
1786 return $this->numberedExpansionCache[$index];
1787 }
1788
1793 public function getNamedArgument( $name ) {
1794 if ( !isset( $this->namedArgs[$name] ) ) {
1795 return false;
1796 }
1797 if ( !isset( $this->namedExpansionCache[$name] ) ) {
1798 # Trim named arguments post-expand, for backwards compatibility
1799 $this->namedExpansionCache[$name] = trim(
1800 $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1801 }
1802 return $this->namedExpansionCache[$name];
1803 }
1804
1809 public function getArgument( $name ) {
1810 $text = $this->getNumberedArgument( $name );
1811 if ( $text === false ) {
1812 $text = $this->getNamedArgument( $name );
1813 }
1814 return $text;
1815 }
1816
1822 public function isTemplate() {
1823 return true;
1824 }
1825
1826 public function setVolatile( $flag = true ) {
1827 parent::setVolatile( $flag );
1828 $this->parent->setVolatile( $flag );
1829 }
1830
1831 public function setTTL( $ttl ) {
1832 parent::setTTL( $ttl );
1833 $this->parent->setTTL( $ttl );
1834 }
1835}
1836
1841// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1842class PPCustomFrame_DOM extends PPFrame_DOM {
1843
1844 public $args;
1845
1846 public function __construct( $preprocessor, $args ) {
1847 parent::__construct( $preprocessor );
1848 $this->args = $args;
1849 }
1850
1851 public function __toString() {
1852 $s = 'cstmframe{';
1853 $first = true;
1854 foreach ( $this->args as $name => $value ) {
1855 if ( $first ) {
1856 $first = false;
1857 } else {
1858 $s .= ', ';
1859 }
1860 $s .= "\"$name\":\"" .
1861 str_replace( '"', '\\"', $value->__toString() ) . '"';
1862 }
1863 $s .= '}';
1864 return $s;
1865 }
1866
1870 public function isEmpty() {
1871 return !count( $this->args );
1872 }
1873
1878 public function getArgument( $index ) {
1879 if ( !isset( $this->args[$index] ) ) {
1880 return false;
1881 }
1882 return $this->args[$index];
1883 }
1884
1885 public function getArguments() {
1886 return $this->args;
1887 }
1888}
1889
1893// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1894class PPNode_DOM implements PPNode {
1895
1899 public $node;
1900 public $xpath;
1901
1902 public function __construct( $node, $xpath = false ) {
1903 $this->node = $node;
1904 }
1905
1909 public function getXPath() {
1910 if ( $this->xpath === null ) {
1911 $this->xpath = new DOMXPath( $this->node->ownerDocument );
1912 }
1913 return $this->xpath;
1914 }
1915
1916 public function __toString() {
1917 if ( $this->node instanceof DOMNodeList ) {
1918 $s = '';
1919 foreach ( $this->node as $node ) {
1920 $s .= $node->ownerDocument->saveXML( $node );
1921 }
1922 } else {
1923 $s = $this->node->ownerDocument->saveXML( $this->node );
1924 }
1925 return $s;
1926 }
1927
1931 public function getChildren() {
1932 return $this->node->childNodes ? new self( $this->node->childNodes ) : false;
1933 }
1934
1938 public function getFirstChild() {
1939 return $this->node->firstChild ? new self( $this->node->firstChild ) : false;
1940 }
1941
1945 public function getNextSibling() {
1946 return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false;
1947 }
1948
1954 public function getChildrenOfType( $type ) {
1955 return new self( $this->getXPath()->query( $type, $this->node ) );
1956 }
1957
1961 public function getLength() {
1962 if ( $this->node instanceof DOMNodeList ) {
1963 return $this->node->length;
1964 } else {
1965 return false;
1966 }
1967 }
1968
1973 public function item( $i ) {
1974 $item = $this->node->item( $i );
1975 return $item ? new self( $item ) : false;
1976 }
1977
1981 public function getName() {
1982 if ( $this->node instanceof DOMNodeList ) {
1983 return '#nodelist';
1984 } else {
1985 return $this->node->nodeName;
1986 }
1987 }
1988
1998 public function splitArg() {
1999 $xpath = $this->getXPath();
2000 $names = $xpath->query( 'name', $this->node );
2001 $values = $xpath->query( 'value', $this->node );
2002 if ( !$names->length || !$values->length ) {
2003 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
2004 }
2005 $name = $names->item( 0 );
2006 $index = $name->getAttribute( 'index' );
2007 return [
2008 'name' => new self( $name ),
2009 'index' => $index,
2010 'value' => new self( $values->item( 0 ) ) ];
2011 }
2012
2020 public function splitExt() {
2021 $xpath = $this->getXPath();
2022 $names = $xpath->query( 'name', $this->node );
2023 $attrs = $xpath->query( 'attr', $this->node );
2024 $inners = $xpath->query( 'inner', $this->node );
2025 $closes = $xpath->query( 'close', $this->node );
2026 if ( !$names->length || !$attrs->length ) {
2027 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
2028 }
2029 $parts = [
2030 'name' => new self( $names->item( 0 ) ),
2031 'attr' => new self( $attrs->item( 0 ) ) ];
2032 if ( $inners->length ) {
2033 $parts['inner'] = new self( $inners->item( 0 ) );
2034 }
2035 if ( $closes->length ) {
2036 $parts['close'] = new self( $closes->item( 0 ) );
2037 }
2038 return $parts;
2039 }
2040
2046 public function splitHeading() {
2047 if ( $this->getName() !== 'h' ) {
2048 throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
2049 }
2050 return [
2051 'i' => $this->node->getAttribute( 'i' ),
2052 'level' => $this->node->getAttribute( 'level' ),
2053 'contents' => $this->getChildren()
2054 ];
2055 }
2056}
$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:68
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:1462
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 since 1.16! 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 since 1.28! 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:1889
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