MediaWiki  master
Preprocessor_DOM.php
Go to the documentation of this file.
1 <?php
27 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
29 
33  public $parser;
34 
35  public $memoryLimit;
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"; # }
223  if ( !$wgDisableLangConversion ) {
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 
831 class 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 
923 class 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 
1032 class 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
1053 class 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, ...$args ) {
1428  $first = true;
1429  $s = '';
1430  foreach ( $args as $root ) {
1431  if ( $root instanceof PPNode_DOM ) {
1432  $root = $root->node;
1433  }
1434  if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1435  $root = [ $root ];
1436  }
1437  foreach ( $root as $node ) {
1438  if ( $first ) {
1439  $first = false;
1440  } else {
1441  $s .= $sep;
1442  }
1443  $s .= $this->expand( $node, $flags );
1444  }
1445  }
1446  return $s;
1447  }
1448 
1457  public function implode( $sep, ...$args ) {
1458  $first = true;
1459  $s = '';
1460  foreach ( $args as $root ) {
1461  if ( $root instanceof PPNode_DOM ) {
1462  $root = $root->node;
1463  }
1464  if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1465  $root = [ $root ];
1466  }
1467  foreach ( $root as $node ) {
1468  if ( $first ) {
1469  $first = false;
1470  } else {
1471  $s .= $sep;
1472  }
1473  $s .= $this->expand( $node );
1474  }
1475  }
1476  return $s;
1477  }
1478 
1487  public function virtualImplode( $sep, ...$args ) {
1488  $out = [];
1489  $first = true;
1490 
1491  foreach ( $args as $root ) {
1492  if ( $root instanceof PPNode_DOM ) {
1493  $root = $root->node;
1494  }
1495  if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1496  $root = [ $root ];
1497  }
1498  foreach ( $root as $node ) {
1499  if ( $first ) {
1500  $first = false;
1501  } else {
1502  $out[] = $sep;
1503  }
1504  $out[] = $node;
1505  }
1506  }
1507  return $out;
1508  }
1509 
1518  public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
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
1664 class 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
1842 class 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  return $this->args[$index] ?? false;
1880  }
1881 
1882  public function getArguments() {
1883  return $this->args;
1884  }
1885 }
1886 
1890 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1891 class PPNode_DOM implements PPNode {
1892 
1896  public $node;
1897  public $xpath;
1898 
1899  public function __construct( $node, $xpath = false ) {
1900  $this->node = $node;
1901  }
1902 
1906  public function getXPath() {
1907  if ( $this->xpath === null ) {
1908  $this->xpath = new DOMXPath( $this->node->ownerDocument );
1909  }
1910  return $this->xpath;
1911  }
1912 
1913  public function __toString() {
1914  if ( $this->node instanceof DOMNodeList ) {
1915  $s = '';
1916  foreach ( $this->node as $node ) {
1917  $s .= $node->ownerDocument->saveXML( $node );
1918  }
1919  } else {
1920  $s = $this->node->ownerDocument->saveXML( $this->node );
1921  }
1922  return $s;
1923  }
1924 
1928  public function getChildren() {
1929  return $this->node->childNodes ? new self( $this->node->childNodes ) : false;
1930  }
1931 
1935  public function getFirstChild() {
1936  return $this->node->firstChild ? new self( $this->node->firstChild ) : false;
1937  }
1938 
1942  public function getNextSibling() {
1943  return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false;
1944  }
1945 
1951  public function getChildrenOfType( $type ) {
1952  return new self( $this->getXPath()->query( $type, $this->node ) );
1953  }
1954 
1958  public function getLength() {
1959  if ( $this->node instanceof DOMNodeList ) {
1960  return $this->node->length;
1961  } else {
1962  return false;
1963  }
1964  }
1965 
1970  public function item( $i ) {
1971  $item = $this->node->item( $i );
1972  return $item ? new self( $item ) : false;
1973  }
1974 
1978  public function getName() {
1979  if ( $this->node instanceof DOMNodeList ) {
1980  return '#nodelist';
1981  } else {
1982  return $this->node->nodeName;
1983  }
1984  }
1985 
1995  public function splitArg() {
1996  $xpath = $this->getXPath();
1997  $names = $xpath->query( 'name', $this->node );
1998  $values = $xpath->query( 'value', $this->node );
1999  if ( !$names->length || !$values->length ) {
2000  throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
2001  }
2002  $name = $names->item( 0 );
2003  $index = $name->getAttribute( 'index' );
2004  return [
2005  'name' => new self( $name ),
2006  'index' => $index,
2007  'value' => new self( $values->item( 0 ) ) ];
2008  }
2009 
2017  public function splitExt() {
2018  $xpath = $this->getXPath();
2019  $names = $xpath->query( 'name', $this->node );
2020  $attrs = $xpath->query( 'attr', $this->node );
2021  $inners = $xpath->query( 'inner', $this->node );
2022  $closes = $xpath->query( 'close', $this->node );
2023  if ( !$names->length || !$attrs->length ) {
2024  throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
2025  }
2026  $parts = [
2027  'name' => new self( $names->item( 0 ) ),
2028  'attr' => new self( $attrs->item( 0 ) ) ];
2029  if ( $inners->length ) {
2030  $parts['inner'] = new self( $inners->item( 0 ) );
2031  }
2032  if ( $closes->length ) {
2033  $parts['close'] = new self( $closes->item( 0 ) );
2034  }
2035  return $parts;
2036  }
2037 
2043  public function splitHeading() {
2044  if ( $this->getName() !== 'h' ) {
2045  throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
2046  }
2047  return [
2048  'i' => $this->node->getAttribute( 'i' ),
2049  'level' => $this->node->getAttribute( 'level' ),
2050  'contents' => $this->getChildren()
2051  ];
2052  }
2053 }
cacheGetTree( $text, $flags)
Attempt to load a precomputed document tree for some given wikitext from 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
Stack class to help Preprocessor::preprocessToObj()
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
DOMElement $node
in this case you re responsible for computing and outputting the entire conflict part
Definition: hooks.txt:1420
There are three types of nodes:
$value
title
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
getArguments()
Returns all arguments of this frame.
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. '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:1980
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
Definition: maintenance.txt:1
if( $line===false) $args
Definition: cdb.php:64
and how to run hooks for an and one after Each event has a name
Definition: hooks.txt:6
__construct( $node, $xpath=false)
splitHeading()
Split a "<h>" node.
Expansion frame with custom arguments.
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:10
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
An expansion frame, used as a context to expand the result of preprocessToObj()
const PTD_FOR_INCLUSION
Definition: Parser.php:107
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
Definition: distributors.txt:9
$wgDisableLangConversion
Whether to enable language variant conversion.
array cacheSetTree( $text, $flags, $tree)
Store a document tree in the cache.
getChildrenOfType( $type)
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:35
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:22
preprocessToObj( $text, $flags=0)
Preprocess some wikitext and return the document tree.
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
splitArg()
Split a "<part>" node into an associative array containing:
newPartNodeArray( $values)
splitExt()
Split an "<ext>" node into an associative array containing name, attr, inner and close All values in ...
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
preprocessToXml( $text, $flags=0)
$matches