MediaWiki REL1_33
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
864 public function getCurrentPart() {
865 if ( $this->top === false ) {
866 return false;
867 } else {
868 return $this->top->getCurrentPart();
869 }
870 }
871
872 public function push( $data ) {
873 if ( $data instanceof $this->elementClass ) {
874 $this->stack[] = $data;
875 } else {
876 $class = $this->elementClass;
877 $this->stack[] = new $class( $data );
878 }
879 $this->top = $this->stack[count( $this->stack ) - 1];
880 $this->accum =& $this->top->getAccum();
881 }
882
883 public function pop() {
884 if ( $this->stack === [] ) {
885 throw new MWException( __METHOD__ . ': no elements remaining' );
886 }
887 $temp = array_pop( $this->stack );
888
889 if ( count( $this->stack ) ) {
890 $this->top = $this->stack[count( $this->stack ) - 1];
891 $this->accum =& $this->top->getAccum();
892 } else {
893 $this->top = self::$false;
894 $this->accum =& $this->rootAccum;
895 }
896 return $temp;
897 }
898
899 public function addPart( $s = '' ) {
900 $this->top->addPart( $s );
901 $this->accum =& $this->top->getAccum();
902 }
903
907 public function getFlags() {
908 if ( $this->stack === [] ) {
909 return [
910 'findEquals' => false,
911 'findPipe' => false,
912 'inHeading' => false,
913 ];
914 } else {
915 return $this->top->getFlags();
916 }
917 }
918}
919
923class PPDStackElement {
927 public $open;
928
932 public $close;
933
938 public $savedPrefix = '';
939
943 public $count;
944
948 public $parts;
949
954 public $lineStart;
955
956 public $partClass = PPDPart::class;
957
958 public function __construct( $data = [] ) {
959 $class = $this->partClass;
960 $this->parts = [ new $class ];
961
962 foreach ( $data as $name => $value ) {
963 $this->$name = $value;
964 }
965 }
966
967 public function &getAccum() {
968 return $this->parts[count( $this->parts ) - 1]->out;
969 }
970
971 public function addPart( $s = '' ) {
972 $class = $this->partClass;
973 $this->parts[] = new $class( $s );
974 }
975
979 public function getCurrentPart() {
980 return $this->parts[count( $this->parts ) - 1];
981 }
982
986 public function getFlags() {
987 $partCount = count( $this->parts );
988 $findPipe = $this->open != "\n" && $this->open != '[';
989 return [
990 'findPipe' => $findPipe,
991 'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ),
992 'inHeading' => $this->open == "\n",
993 ];
994 }
995
1002 public function breakSyntax( $openingCount = false ) {
1003 if ( $this->open == "\n" ) {
1004 $s = $this->savedPrefix . $this->parts[0]->out;
1005 } else {
1006 if ( $openingCount === false ) {
1007 $openingCount = $this->count;
1008 }
1009 $s = substr( $this->open, 0, -1 );
1010 $s .= str_repeat(
1011 substr( $this->open, -1 ),
1012 $openingCount - strlen( $s )
1013 );
1014 $s = $this->savedPrefix . $s;
1015 $first = true;
1016 foreach ( $this->parts as $part ) {
1017 if ( $first ) {
1018 $first = false;
1019 } else {
1020 $s .= '|';
1021 }
1022 $s .= $part->out;
1023 }
1024 }
1025 return $s;
1026 }
1027}
1028
1032class PPDPart {
1036 public $out;
1037
1038 // Optional member variables:
1039 // eqpos Position of equals sign in output accumulator
1040 // commentEnd Past-the-end input pointer for the last comment encountered
1041 // visualEnd Past-the-end input pointer for the end of the accumulator minus comments
1042
1043 public function __construct( $out = '' ) {
1044 $this->out = $out;
1045 }
1046}
1047
1052// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1053class PPFrame_DOM implements PPFrame {
1054
1058 public $preprocessor;
1059
1063 public $parser;
1064
1068 public $title;
1069 public $titleCache;
1070
1075 public $loopCheckHash;
1076
1081 public $depth;
1082
1083 private $volatile = false;
1084 private $ttl = null;
1085
1089 protected $childExpansionCache;
1090
1095 public function __construct( $preprocessor ) {
1096 $this->preprocessor = $preprocessor;
1097 $this->parser = $preprocessor->parser;
1098 $this->title = $this->parser->mTitle;
1099 $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
1100 $this->loopCheckHash = [];
1101 $this->depth = 0;
1102 $this->childExpansionCache = [];
1103 }
1104
1114 public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
1115 $namedArgs = [];
1116 $numberedArgs = [];
1117 if ( $title === false ) {
1118 $title = $this->title;
1119 }
1120 if ( $args !== false ) {
1121 $xpath = false;
1122 if ( $args instanceof PPNode ) {
1123 $args = $args->node;
1124 }
1125 foreach ( $args as $arg ) {
1126 if ( $arg instanceof PPNode ) {
1127 $arg = $arg->node;
1128 }
1129 if ( !$xpath || $xpath->document !== $arg->ownerDocument ) {
1130 $xpath = new DOMXPath( $arg->ownerDocument );
1131 }
1132
1133 $nameNodes = $xpath->query( 'name', $arg );
1134 $value = $xpath->query( 'value', $arg );
1135 if ( $nameNodes->item( 0 )->hasAttributes() ) {
1136 // Numbered parameter
1137 $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
1138 $index = $index - $indexOffset;
1139 if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
1140 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
1141 wfEscapeWikiText( $this->title ),
1142 wfEscapeWikiText( $title ),
1143 wfEscapeWikiText( $index ) )->text() );
1144 $this->parser->addTrackingCategory( 'duplicate-args-category' );
1145 }
1146 $numberedArgs[$index] = $value->item( 0 );
1147 unset( $namedArgs[$index] );
1148 } else {
1149 // Named parameter
1150 $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) );
1151 if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
1152 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
1153 wfEscapeWikiText( $this->title ),
1154 wfEscapeWikiText( $title ),
1155 wfEscapeWikiText( $name ) )->text() );
1156 $this->parser->addTrackingCategory( 'duplicate-args-category' );
1157 }
1158 $namedArgs[$name] = $value->item( 0 );
1159 unset( $numberedArgs[$name] );
1160 }
1161 }
1162 }
1163 return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
1164 }
1165
1173 public function cachedExpand( $key, $root, $flags = 0 ) {
1174 // we don't have a parent, so we don't have a cache
1175 return $this->expand( $root, $flags );
1176 }
1177
1184 public function expand( $root, $flags = 0 ) {
1185 static $expansionDepth = 0;
1186 if ( is_string( $root ) ) {
1187 return $root;
1188 }
1189
1190 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
1191 $this->parser->limitationWarn( 'node-count-exceeded',
1192 $this->parser->mPPNodeCount,
1193 $this->parser->mOptions->getMaxPPNodeCount()
1194 );
1195 return '<span class="error">Node-count limit exceeded</span>';
1196 }
1197
1198 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
1199 $this->parser->limitationWarn( 'expansion-depth-exceeded',
1200 $expansionDepth,
1201 $this->parser->mOptions->getMaxPPExpandDepth()
1202 );
1203 return '<span class="error">Expansion depth limit exceeded</span>';
1204 }
1205 ++$expansionDepth;
1206 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
1207 $this->parser->mHighestExpansionDepth = $expansionDepth;
1208 }
1209
1210 if ( $root instanceof PPNode_DOM ) {
1211 $root = $root->node;
1212 }
1213 if ( $root instanceof DOMDocument ) {
1214 $root = $root->documentElement;
1215 }
1216
1217 $outStack = [ '', '' ];
1218 $iteratorStack = [ false, $root ];
1219 $indexStack = [ 0, 0 ];
1220
1221 while ( count( $iteratorStack ) > 1 ) {
1222 $level = count( $outStack ) - 1;
1223 $iteratorNode =& $iteratorStack[$level];
1224 $out =& $outStack[$level];
1225 $index =& $indexStack[$level];
1226
1227 if ( $iteratorNode instanceof PPNode_DOM ) {
1228 $iteratorNode = $iteratorNode->node;
1229 }
1230
1231 if ( is_array( $iteratorNode ) ) {
1232 if ( $index >= count( $iteratorNode ) ) {
1233 // All done with this iterator
1234 $iteratorStack[$level] = false;
1235 $contextNode = false;
1236 } else {
1237 $contextNode = $iteratorNode[$index];
1238 $index++;
1239 }
1240 } elseif ( $iteratorNode instanceof DOMNodeList ) {
1241 if ( $index >= $iteratorNode->length ) {
1242 // All done with this iterator
1243 $iteratorStack[$level] = false;
1244 $contextNode = false;
1245 } else {
1246 $contextNode = $iteratorNode->item( $index );
1247 $index++;
1248 }
1249 } else {
1250 // Copy to $contextNode and then delete from iterator stack,
1251 // because this is not an iterator but we do have to execute it once
1252 $contextNode = $iteratorStack[$level];
1253 $iteratorStack[$level] = false;
1254 }
1255
1256 if ( $contextNode instanceof PPNode_DOM ) {
1257 $contextNode = $contextNode->node;
1258 }
1259
1260 $newIterator = false;
1261
1262 if ( $contextNode === false ) {
1263 // nothing to do
1264 } elseif ( is_string( $contextNode ) ) {
1265 $out .= $contextNode;
1266 } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) {
1267 $newIterator = $contextNode;
1268 } elseif ( $contextNode instanceof DOMNode ) {
1269 if ( $contextNode->nodeType == XML_TEXT_NODE ) {
1270 $out .= $contextNode->nodeValue;
1271 } elseif ( $contextNode->nodeName == 'template' ) {
1272 # Double-brace expansion
1273 $xpath = new DOMXPath( $contextNode->ownerDocument );
1274 $titles = $xpath->query( 'title', $contextNode );
1275 $title = $titles->item( 0 );
1276 $parts = $xpath->query( 'part', $contextNode );
1277 if ( $flags & PPFrame::NO_TEMPLATES ) {
1278 $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
1279 } else {
1280 $lineStart = $contextNode->getAttribute( 'lineStart' );
1281 $params = [
1282 'title' => new PPNode_DOM( $title ),
1283 'parts' => new PPNode_DOM( $parts ),
1284 'lineStart' => $lineStart ];
1285 $ret = $this->parser->braceSubstitution( $params, $this );
1286 if ( isset( $ret['object'] ) ) {
1287 $newIterator = $ret['object'];
1288 } else {
1289 $out .= $ret['text'];
1290 }
1291 }
1292 } elseif ( $contextNode->nodeName == 'tplarg' ) {
1293 # Triple-brace expansion
1294 $xpath = new DOMXPath( $contextNode->ownerDocument );
1295 $titles = $xpath->query( 'title', $contextNode );
1296 $title = $titles->item( 0 );
1297 $parts = $xpath->query( 'part', $contextNode );
1298 if ( $flags & PPFrame::NO_ARGS ) {
1299 $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
1300 } else {
1301 $params = [
1302 'title' => new PPNode_DOM( $title ),
1303 'parts' => new PPNode_DOM( $parts ) ];
1304 $ret = $this->parser->argSubstitution( $params, $this );
1305 if ( isset( $ret['object'] ) ) {
1306 $newIterator = $ret['object'];
1307 } else {
1308 $out .= $ret['text'];
1309 }
1310 }
1311 } elseif ( $contextNode->nodeName == 'comment' ) {
1312 # HTML-style comment
1313 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1314 # Not in RECOVER_COMMENTS mode (msgnw) though.
1315 if ( ( $this->parser->ot['html']
1316 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1317 || ( $flags & PPFrame::STRIP_COMMENTS )
1318 ) && !( $flags & PPFrame::RECOVER_COMMENTS )
1319 ) {
1320 $out .= '';
1321 } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1322 # Add a strip marker in PST mode so that pstPass2() can
1323 # run some old-fashioned regexes on the result.
1324 # Not in RECOVER_COMMENTS mode (extractSections) though.
1325 $out .= $this->parser->insertStripItem( $contextNode->textContent );
1326 } else {
1327 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1328 $out .= $contextNode->textContent;
1329 }
1330 } elseif ( $contextNode->nodeName == 'ignore' ) {
1331 # Output suppression used by <includeonly> etc.
1332 # OT_WIKI will only respect <ignore> in substed templates.
1333 # The other output types respect it unless NO_IGNORE is set.
1334 # extractSections() sets NO_IGNORE and so never respects it.
1335 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1336 || ( $flags & PPFrame::NO_IGNORE )
1337 ) {
1338 $out .= $contextNode->textContent;
1339 } else {
1340 $out .= '';
1341 }
1342 } elseif ( $contextNode->nodeName == 'ext' ) {
1343 # Extension tag
1344 $xpath = new DOMXPath( $contextNode->ownerDocument );
1345 $names = $xpath->query( 'name', $contextNode );
1346 $attrs = $xpath->query( 'attr', $contextNode );
1347 $inners = $xpath->query( 'inner', $contextNode );
1348 $closes = $xpath->query( 'close', $contextNode );
1349 if ( $flags & PPFrame::NO_TAGS ) {
1350 $s = '<' . $this->expand( $names->item( 0 ), $flags );
1351 if ( $attrs->length > 0 ) {
1352 $s .= $this->expand( $attrs->item( 0 ), $flags );
1353 }
1354 if ( $inners->length > 0 ) {
1355 $s .= '>' . $this->expand( $inners->item( 0 ), $flags );
1356 if ( $closes->length > 0 ) {
1357 $s .= $this->expand( $closes->item( 0 ), $flags );
1358 }
1359 } else {
1360 $s .= '/>';
1361 }
1362 $out .= $s;
1363 } else {
1364 $params = [
1365 'name' => new PPNode_DOM( $names->item( 0 ) ),
1366 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null,
1367 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null,
1368 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null,
1369 ];
1370 $out .= $this->parser->extensionSubstitution( $params, $this );
1371 }
1372 } elseif ( $contextNode->nodeName == 'h' ) {
1373 # Heading
1374 $s = $this->expand( $contextNode->childNodes, $flags );
1375
1376 # Insert a heading marker only for <h> children of <root>
1377 # This is to stop extractSections from going over multiple tree levels
1378 if ( $contextNode->parentNode->nodeName == 'root' && $this->parser->ot['html'] ) {
1379 # Insert heading index marker
1380 $headingIndex = $contextNode->getAttribute( 'i' );
1381 $titleText = $this->title->getPrefixedDBkey();
1382 $this->parser->mHeadings[] = [ $titleText, $headingIndex ];
1383 $serial = count( $this->parser->mHeadings ) - 1;
1384 $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
1385 $count = $contextNode->getAttribute( 'level' );
1386 $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
1387 $this->parser->mStripState->addGeneral( $marker, '' );
1388 }
1389 $out .= $s;
1390 } else {
1391 # Generic recursive expansion
1392 $newIterator = $contextNode->childNodes;
1393 }
1394 } else {
1395 throw new MWException( __METHOD__ . ': Invalid parameter type' );
1396 }
1397
1398 if ( $newIterator !== false ) {
1399 if ( $newIterator instanceof PPNode_DOM ) {
1400 $newIterator = $newIterator->node;
1401 }
1402 $outStack[] = '';
1403 $iteratorStack[] = $newIterator;
1404 $indexStack[] = 0;
1405 } elseif ( $iteratorStack[$level] === false ) {
1406 // Return accumulated value to parent
1407 // With tail recursion
1408 while ( $iteratorStack[$level] === false && $level > 0 ) {
1409 $outStack[$level - 1] .= $out;
1410 array_pop( $outStack );
1411 array_pop( $iteratorStack );
1412 array_pop( $indexStack );
1413 $level--;
1414 }
1415 }
1416 }
1417 --$expansionDepth;
1418 return $outStack[0];
1419 }
1420
1427 public function implodeWithFlags( $sep, $flags /*, ... */ ) {
1428 $args = array_slice( func_get_args(), 2 );
1429
1430 $first = true;
1431 $s = '';
1432 foreach ( $args as $root ) {
1433 if ( $root instanceof PPNode_DOM ) {
1434 $root = $root->node;
1435 }
1436 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1437 $root = [ $root ];
1438 }
1439 foreach ( $root as $node ) {
1440 if ( $first ) {
1441 $first = false;
1442 } else {
1443 $s .= $sep;
1444 }
1445 $s .= $this->expand( $node, $flags );
1446 }
1447 }
1448 return $s;
1449 }
1450
1459 public function implode( $sep /*, ... */ ) {
1460 $args = array_slice( func_get_args(), 1 );
1461
1462 $first = true;
1463 $s = '';
1464 foreach ( $args as $root ) {
1465 if ( $root instanceof PPNode_DOM ) {
1466 $root = $root->node;
1467 }
1468 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1469 $root = [ $root ];
1470 }
1471 foreach ( $root as $node ) {
1472 if ( $first ) {
1473 $first = false;
1474 } else {
1475 $s .= $sep;
1476 }
1477 $s .= $this->expand( $node );
1478 }
1479 }
1480 return $s;
1481 }
1482
1491 public function virtualImplode( $sep /*, ... */ ) {
1492 $args = array_slice( func_get_args(), 1 );
1493 $out = [];
1494 $first = true;
1495
1496 foreach ( $args as $root ) {
1497 if ( $root instanceof PPNode_DOM ) {
1498 $root = $root->node;
1499 }
1500 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1501 $root = [ $root ];
1502 }
1503 foreach ( $root as $node ) {
1504 if ( $first ) {
1505 $first = false;
1506 } else {
1507 $out[] = $sep;
1508 }
1509 $out[] = $node;
1510 }
1511 }
1512 return $out;
1513 }
1514
1523 public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1524 $args = array_slice( func_get_args(), 3 );
1525 $out = [ $start ];
1526 $first = true;
1527
1528 foreach ( $args as $root ) {
1529 if ( $root instanceof PPNode_DOM ) {
1530 $root = $root->node;
1531 }
1532 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1533 $root = [ $root ];
1534 }
1535 foreach ( $root as $node ) {
1536 if ( $first ) {
1537 $first = false;
1538 } else {
1539 $out[] = $sep;
1540 }
1541 $out[] = $node;
1542 }
1543 }
1544 $out[] = $end;
1545 return $out;
1546 }
1547
1548 public function __toString() {
1549 return 'frame{}';
1550 }
1551
1552 public function getPDBK( $level = false ) {
1553 if ( $level === false ) {
1554 return $this->title->getPrefixedDBkey();
1555 } else {
1556 return $this->titleCache[$level] ?? false;
1557 }
1558 }
1559
1563 public function getArguments() {
1564 return [];
1565 }
1566
1570 public function getNumberedArguments() {
1571 return [];
1572 }
1573
1577 public function getNamedArguments() {
1578 return [];
1579 }
1580
1586 public function isEmpty() {
1587 return true;
1588 }
1589
1594 public function getArgument( $name ) {
1595 return false;
1596 }
1597
1604 public function loopCheck( $title ) {
1605 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1606 }
1607
1613 public function isTemplate() {
1614 return false;
1615 }
1616
1622 public function getTitle() {
1623 return $this->title;
1624 }
1625
1631 public function setVolatile( $flag = true ) {
1632 $this->volatile = $flag;
1633 }
1634
1640 public function isVolatile() {
1641 return $this->volatile;
1642 }
1643
1649 public function setTTL( $ttl ) {
1650 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1651 $this->ttl = $ttl;
1652 }
1653 }
1654
1660 public function getTTL() {
1661 return $this->ttl;
1662 }
1663}
1664
1669// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1670class PPTemplateFrame_DOM extends PPFrame_DOM {
1671
1672 public $numberedArgs, $namedArgs;
1673
1677 public $parent;
1678 public $numberedExpansionCache, $namedExpansionCache;
1679
1687 public function __construct( $preprocessor, $parent = false, $numberedArgs = [],
1688 $namedArgs = [], $title = false
1689 ) {
1690 parent::__construct( $preprocessor );
1691
1692 $this->parent = $parent;
1693 $this->numberedArgs = $numberedArgs;
1694 $this->namedArgs = $namedArgs;
1695 $this->title = $title;
1696 $pdbk = $title ? $title->getPrefixedDBkey() : false;
1697 $this->titleCache = $parent->titleCache;
1698 $this->titleCache[] = $pdbk;
1699 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1700 if ( $pdbk !== false ) {
1701 $this->loopCheckHash[$pdbk] = true;
1702 }
1703 $this->depth = $parent->depth + 1;
1704 $this->numberedExpansionCache = $this->namedExpansionCache = [];
1705 }
1706
1707 public function __toString() {
1708 $s = 'tplframe{';
1709 $first = true;
1710 $args = $this->numberedArgs + $this->namedArgs;
1711 foreach ( $args as $name => $value ) {
1712 if ( $first ) {
1713 $first = false;
1714 } else {
1715 $s .= ', ';
1716 }
1717 $s .= "\"$name\":\"" .
1718 str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"';
1719 }
1720 $s .= '}';
1721 return $s;
1722 }
1723
1731 public function cachedExpand( $key, $root, $flags = 0 ) {
1732 if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1733 return $this->parent->childExpansionCache[$key];
1734 }
1735 $retval = $this->expand( $root, $flags );
1736 if ( !$this->isVolatile() ) {
1737 $this->parent->childExpansionCache[$key] = $retval;
1738 }
1739 return $retval;
1740 }
1741
1747 public function isEmpty() {
1748 return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1749 }
1750
1751 public function getArguments() {
1752 $arguments = [];
1753 foreach ( array_merge(
1754 array_keys( $this->numberedArgs ),
1755 array_keys( $this->namedArgs ) ) as $key ) {
1756 $arguments[$key] = $this->getArgument( $key );
1757 }
1758 return $arguments;
1759 }
1760
1761 public function getNumberedArguments() {
1762 $arguments = [];
1763 foreach ( array_keys( $this->numberedArgs ) as $key ) {
1764 $arguments[$key] = $this->getArgument( $key );
1765 }
1766 return $arguments;
1767 }
1768
1769 public function getNamedArguments() {
1770 $arguments = [];
1771 foreach ( array_keys( $this->namedArgs ) as $key ) {
1772 $arguments[$key] = $this->getArgument( $key );
1773 }
1774 return $arguments;
1775 }
1776
1781 public function getNumberedArgument( $index ) {
1782 if ( !isset( $this->numberedArgs[$index] ) ) {
1783 return false;
1784 }
1785 if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1786 # No trimming for unnamed arguments
1787 $this->numberedExpansionCache[$index] = $this->parent->expand(
1788 $this->numberedArgs[$index],
1789 PPFrame::STRIP_COMMENTS
1790 );
1791 }
1792 return $this->numberedExpansionCache[$index];
1793 }
1794
1799 public function getNamedArgument( $name ) {
1800 if ( !isset( $this->namedArgs[$name] ) ) {
1801 return false;
1802 }
1803 if ( !isset( $this->namedExpansionCache[$name] ) ) {
1804 # Trim named arguments post-expand, for backwards compatibility
1805 $this->namedExpansionCache[$name] = trim(
1806 $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1807 }
1808 return $this->namedExpansionCache[$name];
1809 }
1810
1815 public function getArgument( $name ) {
1816 $text = $this->getNumberedArgument( $name );
1817 if ( $text === false ) {
1818 $text = $this->getNamedArgument( $name );
1819 }
1820 return $text;
1821 }
1822
1828 public function isTemplate() {
1829 return true;
1830 }
1831
1832 public function setVolatile( $flag = true ) {
1833 parent::setVolatile( $flag );
1834 $this->parent->setVolatile( $flag );
1835 }
1836
1837 public function setTTL( $ttl ) {
1838 parent::setTTL( $ttl );
1839 $this->parent->setTTL( $ttl );
1840 }
1841}
1842
1847// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1848class PPCustomFrame_DOM extends PPFrame_DOM {
1849
1850 public $args;
1851
1852 public function __construct( $preprocessor, $args ) {
1853 parent::__construct( $preprocessor );
1854 $this->args = $args;
1855 }
1856
1857 public function __toString() {
1858 $s = 'cstmframe{';
1859 $first = true;
1860 foreach ( $this->args as $name => $value ) {
1861 if ( $first ) {
1862 $first = false;
1863 } else {
1864 $s .= ', ';
1865 }
1866 $s .= "\"$name\":\"" .
1867 str_replace( '"', '\\"', $value->__toString() ) . '"';
1868 }
1869 $s .= '}';
1870 return $s;
1871 }
1872
1876 public function isEmpty() {
1877 return !count( $this->args );
1878 }
1879
1884 public function getArgument( $index ) {
1885 return $this->args[$index] ?? false;
1886 }
1887
1888 public function getArguments() {
1889 return $this->args;
1890 }
1891}
1892
1896// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1897class PPNode_DOM implements PPNode {
1898
1902 public $node;
1903 public $xpath;
1904
1905 public function __construct( $node, $xpath = false ) {
1906 $this->node = $node;
1907 }
1908
1912 public function getXPath() {
1913 if ( $this->xpath === null ) {
1914 $this->xpath = new DOMXPath( $this->node->ownerDocument );
1915 }
1916 return $this->xpath;
1917 }
1918
1919 public function __toString() {
1920 if ( $this->node instanceof DOMNodeList ) {
1921 $s = '';
1922 foreach ( $this->node as $node ) {
1923 $s .= $node->ownerDocument->saveXML( $node );
1924 }
1925 } else {
1926 $s = $this->node->ownerDocument->saveXML( $this->node );
1927 }
1928 return $s;
1929 }
1930
1934 public function getChildren() {
1935 return $this->node->childNodes ? new self( $this->node->childNodes ) : false;
1936 }
1937
1941 public function getFirstChild() {
1942 return $this->node->firstChild ? new self( $this->node->firstChild ) : false;
1943 }
1944
1948 public function getNextSibling() {
1949 return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false;
1950 }
1951
1957 public function getChildrenOfType( $type ) {
1958 return new self( $this->getXPath()->query( $type, $this->node ) );
1959 }
1960
1964 public function getLength() {
1965 if ( $this->node instanceof DOMNodeList ) {
1966 return $this->node->length;
1967 } else {
1968 return false;
1969 }
1970 }
1971
1976 public function item( $i ) {
1977 $item = $this->node->item( $i );
1978 return $item ? new self( $item ) : false;
1979 }
1980
1984 public function getName() {
1985 if ( $this->node instanceof DOMNodeList ) {
1986 return '#nodelist';
1987 } else {
1988 return $this->node->nodeName;
1989 }
1990 }
1991
2001 public function splitArg() {
2002 $xpath = $this->getXPath();
2003 $names = $xpath->query( 'name', $this->node );
2004 $values = $xpath->query( 'value', $this->node );
2005 if ( !$names->length || !$values->length ) {
2006 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
2007 }
2008 $name = $names->item( 0 );
2009 $index = $name->getAttribute( 'index' );
2010 return [
2011 'name' => new self( $name ),
2012 'index' => $index,
2013 'value' => new self( $values->item( 0 ) ) ];
2014 }
2015
2023 public function splitExt() {
2024 $xpath = $this->getXPath();
2025 $names = $xpath->query( 'name', $this->node );
2026 $attrs = $xpath->query( 'attr', $this->node );
2027 $inners = $xpath->query( 'inner', $this->node );
2028 $closes = $xpath->query( 'close', $this->node );
2029 if ( !$names->length || !$attrs->length ) {
2030 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
2031 }
2032 $parts = [
2033 'name' => new self( $names->item( 0 ) ),
2034 'attr' => new self( $attrs->item( 0 ) ) ];
2035 if ( $inners->length ) {
2036 $parts['inner'] = new self( $inners->item( 0 ) );
2037 }
2038 if ( $closes->length ) {
2039 $parts['close'] = new self( $closes->item( 0 ) );
2040 }
2041 return $parts;
2042 }
2043
2049 public function splitHeading() {
2050 if ( $this->getName() !== 'h' ) {
2051 throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
2052 }
2053 return [
2054 'i' => $this->node->getAttribute( 'i' ),
2055 'level' => $this->node->getAttribute( 'level' ),
2056 'contents' => $this->getChildren()
2057 ];
2058 }
2059}
within a display generated by the Derivative if and wherever such third party notices normally appear The contents of the NOTICE file are for informational purposes only and do not modify the License You may add Your own attribution notices within Derivative Works that You alongside or as an addendum to the NOTICE text from the provided that such additional attribution notices cannot be construed as modifying the License You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for or distribution of Your or for any such Derivative Works as a provided Your and distribution of the Work otherwise complies with the conditions stated in this License Submission of Contributions Unless You explicitly state any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this without any additional terms or conditions Notwithstanding the nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions Trademarks This License does not grant permission to use the trade names
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
$wgDisableLangConversion
Whether to enable language variant conversion.
if( $line===false) $args
Definition cdb.php:64
MediaWiki exception.
Expansion frame with custom arguments.
Stack class to help Preprocessor::preprocessToObj()
An expansion frame, used as a context to expand the result of preprocessToObj()
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:69
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 as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same so they can t rely on Unix and must forbid reads to even standard directories like tmp lest users read each others files We cannot assume that the user has the ability to install or run any programs not written as web accessible PHP scripts Since anything that works on cheap shared hosting will work if you have shell or root access MediaWiki s design is based around catering to the lowest common denominator Although we support higher end setups as the way many things work by default is tailored toward shared hosting These defaults are unconventional from the point of view of and they certainly aren t ideal for someone who s installing MediaWiki as root
in this case you re responsible for computing and outputting the entire conflict part
Definition hooks.txt:1423
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImgAuthModifyHeaders':Executed just before a file is streamed to a user via img_auth.php, allowing headers to be modified beforehand. $title:LinkTarget object & $headers:HTTP headers(name=> 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. '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 '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) $result
Definition hooks.txt:1991
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:271
and how to run hooks for an and one after Each event has a name
Definition hooks.txt:12
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to and or sell copies of the and to permit persons to whom the Software is furnished to do so
Definition LICENSE.txt:13
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
There are three types of nodes:
Prior to maintenance scripts were a hodgepodge of code that had no cohesion or formal method of action Beginning maintenance scripts have been cleaned up to use a unified class Directory structure How to run a script How to write your own DIRECTORY STRUCTURE The maintenance directory of a MediaWiki installation contains several all of which have unique purposes HOW TO RUN A SCRIPT Ridiculously just call php someScript php that s in the top level maintenance directory if not default wiki
title
This document describes the state of Postgres support in and is fairly well maintained The main code is very well while extensions are very hit and miss it is probably the most supported database after MySQL Much of the work in making MediaWiki database agnostic came about through the work of creating Postgres but without copying over all the usage comments General notes on the but these can almost always be programmed around *Although Postgres has a true BOOLEAN type
Definition postgres.txt:30