MediaWiki  1.27.1
Preprocessor_DOM.php
Go to the documentation of this file.
1 <?php
27 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
29  // @codingStandardsIgnoreEnd
30 
34  public $parser;
35 
36  public $memoryLimit;
37 
38  const CACHE_PREFIX = 'preprocess-xml';
39 
40  public function __construct( $parser ) {
41  $this->parser = $parser;
42  $mem = ini_get( 'memory_limit' );
43  $this->memoryLimit = false;
44  if ( strval( $mem ) !== '' && $mem != -1 ) {
45  if ( preg_match( '/^\d+$/', $mem ) ) {
46  $this->memoryLimit = $mem;
47  } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) {
48  $this->memoryLimit = $m[1] * 1048576;
49  }
50  }
51  }
52 
56  public function newFrame() {
57  return new PPFrame_DOM( $this );
58  }
59 
64  public function newCustomFrame( $args ) {
65  return new PPCustomFrame_DOM( $this, $args );
66  }
67 
73  public function newPartNodeArray( $values ) {
74  // NOTE: DOM manipulation is slower than building & parsing XML! (or so Tim sais)
75  $xml = "<list>";
76 
77  foreach ( $values as $k => $val ) {
78  if ( is_int( $k ) ) {
79  $xml .= "<part><name index=\"$k\"/><value>"
80  . htmlspecialchars( $val ) . "</value></part>";
81  } else {
82  $xml .= "<part><name>" . htmlspecialchars( $k )
83  . "</name>=<value>" . htmlspecialchars( $val ) . "</value></part>";
84  }
85  }
86 
87  $xml .= "</list>";
88 
89  $dom = new DOMDocument();
90  MediaWiki\suppressWarnings();
91  $result = $dom->loadXML( $xml );
92  MediaWiki\restoreWarnings();
93  if ( !$result ) {
94  // Try running the XML through UtfNormal to get rid of invalid characters
95  $xml = UtfNormal\Validator::cleanUp( $xml );
96  // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2
97  // don't barf when the XML is >256 levels deep
98  $result = $dom->loadXML( $xml, 1 << 19 );
99  }
100 
101  if ( !$result ) {
102  throw new MWException( 'Parameters passed to ' . __METHOD__ . ' result in invalid XML' );
103  }
104 
105  $root = $dom->documentElement;
106  $node = new PPNode_DOM( $root->childNodes );
107  return $node;
108  }
109 
114  public function memCheck() {
115  if ( $this->memoryLimit === false ) {
116  return true;
117  }
118  $usage = memory_get_usage();
119  if ( $usage > $this->memoryLimit * 0.9 ) {
120  $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 );
121  throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" );
122  }
123  return $usage <= $this->memoryLimit * 0.8;
124  }
125 
150  public function preprocessToObj( $text, $flags = 0 ) {
151 
152  $xml = $this->cacheGetTree( $text, $flags );
153  if ( $xml === false ) {
154  $xml = $this->preprocessToXml( $text, $flags );
155  $this->cacheSetTree( $text, $flags, $xml );
156  }
157 
158  // Fail if the number of elements exceeds acceptable limits
159  // Do not attempt to generate the DOM
160  $this->parser->mGeneratedPPNodeCount += substr_count( $xml, '<' );
161  $max = $this->parser->mOptions->getMaxGeneratedPPNodeCount();
162  if ( $this->parser->mGeneratedPPNodeCount > $max ) {
163  // if ( $cacheable ) { ... }
164  throw new MWException( __METHOD__ . ': generated node count limit exceeded' );
165  }
166 
167  $dom = new DOMDocument;
168  MediaWiki\suppressWarnings();
169  $result = $dom->loadXML( $xml );
170  MediaWiki\restoreWarnings();
171  if ( !$result ) {
172  // Try running the XML through UtfNormal to get rid of invalid characters
173  $xml = UtfNormal\Validator::cleanUp( $xml );
174  // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2
175  // don't barf when the XML is >256 levels deep.
176  $result = $dom->loadXML( $xml, 1 << 19 );
177  }
178  if ( $result ) {
179  $obj = new PPNode_DOM( $dom->documentElement );
180  }
181 
182  // if ( $cacheable ) { ... }
183 
184  if ( !$result ) {
185  throw new MWException( __METHOD__ . ' generated invalid XML' );
186  }
187  return $obj;
188  }
189 
195  public function preprocessToXml( $text, $flags = 0 ) {
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  // For fast reverse searches
224  $revText = strrev( $text );
225  $lengthText = strlen( $text );
226 
227  // Input pointer, starts out pointing to a pseudo-newline before the start
228  $i = 0;
229  // Current accumulator
230  $accum =& $stack->getAccum();
231  $accum = '<root>';
232  // True to find equals signs in arguments
233  $findEquals = false;
234  // True to take notice of pipe characters
235  $findPipe = false;
236  $headingIndex = 1;
237  // True if $i is inside a possible heading
238  $inHeading = false;
239  // True if there are no more greater-than (>) signs right of $i
240  $noMoreGT = false;
241  // Map of tag name => true if there are no more closing tags of given type right of $i
242  $noMoreClosingTag = [];
243  // True to ignore all input up to the next <onlyinclude>
244  $findOnlyinclude = $enableOnlyinclude;
245  // Do a line-start run without outputting an LF character
246  $fakeLineStart = true;
247 
248  while ( true ) {
249  // $this->memCheck();
250 
251  if ( $findOnlyinclude ) {
252  // Ignore all input up to the next <onlyinclude>
253  $startPos = strpos( $text, '<onlyinclude>', $i );
254  if ( $startPos === false ) {
255  // Ignored section runs to the end
256  $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>';
257  break;
258  }
259  $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
260  $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>';
261  $i = $tagEndPos;
262  $findOnlyinclude = false;
263  }
264 
265  if ( $fakeLineStart ) {
266  $found = 'line-start';
267  $curChar = '';
268  } else {
269  # Find next opening brace, closing brace or pipe
270  $search = $searchBase;
271  if ( $stack->top === false ) {
272  $currentClosing = '';
273  } else {
274  $currentClosing = $stack->top->close;
275  $search .= $currentClosing;
276  }
277  if ( $findPipe ) {
278  $search .= '|';
279  }
280  if ( $findEquals ) {
281  // First equals will be for the template
282  $search .= '=';
283  }
284  $rule = null;
285  # Output literal section, advance input counter
286  $literalLength = strcspn( $text, $search, $i );
287  if ( $literalLength > 0 ) {
288  $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) );
289  $i += $literalLength;
290  }
291  if ( $i >= $lengthText ) {
292  if ( $currentClosing == "\n" ) {
293  // Do a past-the-end run to finish off the heading
294  $curChar = '';
295  $found = 'line-end';
296  } else {
297  # All done
298  break;
299  }
300  } else {
301  $curChar = $text[$i];
302  if ( $curChar == '|' ) {
303  $found = 'pipe';
304  } elseif ( $curChar == '=' ) {
305  $found = 'equals';
306  } elseif ( $curChar == '<' ) {
307  $found = 'angle';
308  } elseif ( $curChar == "\n" ) {
309  if ( $inHeading ) {
310  $found = 'line-end';
311  } else {
312  $found = 'line-start';
313  }
314  } elseif ( $curChar == $currentClosing ) {
315  $found = 'close';
316  } elseif ( isset( $this->rules[$curChar] ) ) {
317  $found = 'open';
318  $rule = $this->rules[$curChar];
319  } else {
320  # Some versions of PHP have a strcspn which stops on null characters
321  # Ignore and continue
322  ++$i;
323  continue;
324  }
325  }
326  }
327 
328  if ( $found == 'angle' ) {
329  $matches = false;
330  // Handle </onlyinclude>
331  if ( $enableOnlyinclude
332  && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>'
333  ) {
334  $findOnlyinclude = true;
335  continue;
336  }
337 
338  // Determine element name
339  if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
340  // Element name missing or not listed
341  $accum .= '&lt;';
342  ++$i;
343  continue;
344  }
345  // Handle comments
346  if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
347 
348  // To avoid leaving blank lines, when a sequence of
349  // space-separated comments is both preceded and followed by
350  // a newline (ignoring spaces), then
351  // trim leading and trailing spaces and the trailing newline.
352 
353  // Find the end
354  $endPos = strpos( $text, '-->', $i + 4 );
355  if ( $endPos === false ) {
356  // Unclosed comment in input, runs to end
357  $inner = substr( $text, $i );
358  $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
359  $i = $lengthText;
360  } else {
361  // Search backwards for leading whitespace
362  $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0;
363 
364  // Search forwards for trailing whitespace
365  // $wsEnd will be the position of the last space (or the '>' if there's none)
366  $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 );
367 
368  // Keep looking forward as long as we're finding more
369  // comments.
370  $comments = [ [ $wsStart, $wsEnd ] ];
371  while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) {
372  $c = strpos( $text, '-->', $wsEnd + 4 );
373  if ( $c === false ) {
374  break;
375  }
376  $c = $c + 2 + strspn( $text, " \t", $c + 3 );
377  $comments[] = [ $wsEnd + 1, $c ];
378  $wsEnd = $c;
379  }
380 
381  // Eat the line if possible
382  // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
383  // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
384  // it's a possible beneficial b/c break.
385  if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
386  && substr( $text, $wsEnd + 1, 1 ) == "\n"
387  ) {
388  // Remove leading whitespace from the end of the accumulator
389  // Sanity check first though
390  $wsLength = $i - $wsStart;
391  if ( $wsLength > 0
392  && strspn( $accum, " \t", -$wsLength ) === $wsLength
393  ) {
394  $accum = substr( $accum, 0, -$wsLength );
395  }
396 
397  // Dump all but the last comment to the accumulator
398  foreach ( $comments as $j => $com ) {
399  $startPos = $com[0];
400  $endPos = $com[1] + 1;
401  if ( $j == ( count( $comments ) - 1 ) ) {
402  break;
403  }
404  $inner = substr( $text, $startPos, $endPos - $startPos );
405  $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
406  }
407 
408  // Do a line-start run next time to look for headings after the comment
409  $fakeLineStart = true;
410  } else {
411  // No line to eat, just take the comment itself
412  $startPos = $i;
413  $endPos += 2;
414  }
415 
416  if ( $stack->top ) {
417  $part = $stack->top->getCurrentPart();
418  if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) {
419  $part->visualEnd = $wsStart;
420  }
421  // Else comments abutting, no change in visual end
422  $part->commentEnd = $endPos;
423  }
424  $i = $endPos + 1;
425  $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
426  $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
427  }
428  continue;
429  }
430  $name = $matches[1];
431  $lowerName = strtolower( $name );
432  $attrStart = $i + strlen( $name ) + 1;
433 
434  // Find end of tag
435  $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
436  if ( $tagEndPos === false ) {
437  // Infinite backtrack
438  // Disable tag search to prevent worst-case O(N^2) performance
439  $noMoreGT = true;
440  $accum .= '&lt;';
441  ++$i;
442  continue;
443  }
444 
445  // Handle ignored tags
446  if ( in_array( $lowerName, $ignoredTags ) ) {
447  $accum .= '<ignore>'
448  . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) )
449  . '</ignore>';
450  $i = $tagEndPos + 1;
451  continue;
452  }
453 
454  $tagStartPos = $i;
455  if ( $text[$tagEndPos - 1] == '/' ) {
456  $attrEnd = $tagEndPos - 1;
457  $inner = null;
458  $i = $tagEndPos + 1;
459  $close = '';
460  } else {
461  $attrEnd = $tagEndPos;
462  // Find closing tag
463  if (
464  !isset( $noMoreClosingTag[$name] ) &&
465  preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
466  $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 )
467  ) {
468  $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
469  $i = $matches[0][1] + strlen( $matches[0][0] );
470  $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>';
471  } else {
472  // No end tag
473  if ( in_array( $name, $xmlishAllowMissingEndTag ) ) {
474  // Let it run out to the end of the text.
475  $inner = substr( $text, $tagEndPos + 1 );
476  $i = $lengthText;
477  $close = '';
478  } else {
479  // Don't match the tag, treat opening tag as literal and resume parsing.
480  $i = $tagEndPos + 1;
481  $accum .= htmlspecialchars( substr( $text, $tagStartPos, $tagEndPos + 1 - $tagStartPos ) );
482  // Cache results, otherwise we have O(N^2) performance for input like <foo><foo><foo>...
483  $noMoreClosingTag[$name] = true;
484  continue;
485  }
486  }
487  }
488  // <includeonly> and <noinclude> just become <ignore> tags
489  if ( in_array( $lowerName, $ignoredElements ) ) {
490  $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) )
491  . '</ignore>';
492  continue;
493  }
494 
495  $accum .= '<ext>';
496  if ( $attrEnd <= $attrStart ) {
497  $attr = '';
498  } else {
499  $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
500  }
501  $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' .
502  // Note that the attr element contains the whitespace between name and attribute,
503  // this is necessary for precise reconstruction during pre-save transform.
504  '<attr>' . htmlspecialchars( $attr ) . '</attr>';
505  if ( $inner !== null ) {
506  $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>';
507  }
508  $accum .= $close . '</ext>';
509  } elseif ( $found == 'line-start' ) {
510  // Is this the start of a heading?
511  // Line break belongs before the heading element in any case
512  if ( $fakeLineStart ) {
513  $fakeLineStart = false;
514  } else {
515  $accum .= $curChar;
516  $i++;
517  }
518 
519  $count = strspn( $text, '=', $i, 6 );
520  if ( $count == 1 && $findEquals ) {
521  // DWIM: This looks kind of like a name/value separator.
522  // Let's let the equals handler have it and break the
523  // potential heading. This is heuristic, but AFAICT the
524  // methods for completely correct disambiguation are very
525  // complex.
526  } elseif ( $count > 0 ) {
527  $piece = [
528  'open' => "\n",
529  'close' => "\n",
530  'parts' => [ new PPDPart( str_repeat( '=', $count ) ) ],
531  'startPos' => $i,
532  'count' => $count ];
533  $stack->push( $piece );
534  $accum =& $stack->getAccum();
535  $flags = $stack->getFlags();
536  extract( $flags );
537  $i += $count;
538  }
539  } elseif ( $found == 'line-end' ) {
540  $piece = $stack->top;
541  // A heading must be open, otherwise \n wouldn't have been in the search list
542  assert( '$piece->open == "\n"' );
543  $part = $piece->getCurrentPart();
544  // Search back through the input to see if it has a proper close.
545  // Do this using the reversed string since the other solutions
546  // (end anchor, etc.) are inefficient.
547  $wsLength = strspn( $revText, " \t", $lengthText - $i );
548  $searchStart = $i - $wsLength;
549  if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
550  // Comment found at line end
551  // Search for equals signs before the comment
552  $searchStart = $part->visualEnd;
553  $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
554  }
555  $count = $piece->count;
556  $equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
557  if ( $equalsLength > 0 ) {
558  if ( $searchStart - $equalsLength == $piece->startPos ) {
559  // This is just a single string of equals signs on its own line
560  // Replicate the doHeadings behavior /={count}(.+)={count}/
561  // First find out how many equals signs there really are (don't stop at 6)
562  $count = $equalsLength;
563  if ( $count < 3 ) {
564  $count = 0;
565  } else {
566  $count = min( 6, intval( ( $count - 1 ) / 2 ) );
567  }
568  } else {
569  $count = min( $equalsLength, $count );
570  }
571  if ( $count > 0 ) {
572  // Normal match, output <h>
573  $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>";
574  $headingIndex++;
575  } else {
576  // Single equals sign on its own line, count=0
577  $element = $accum;
578  }
579  } else {
580  // No match, no <h>, just pass down the inner text
581  $element = $accum;
582  }
583  // Unwind the stack
584  $stack->pop();
585  $accum =& $stack->getAccum();
586  $flags = $stack->getFlags();
587  extract( $flags );
588 
589  // Append the result to the enclosing accumulator
590  $accum .= $element;
591  // Note that we do NOT increment the input pointer.
592  // This is because the closing linebreak could be the opening linebreak of
593  // another heading. Infinite loops are avoided because the next iteration MUST
594  // hit the heading open case above, which unconditionally increments the
595  // input pointer.
596  } elseif ( $found == 'open' ) {
597  # count opening brace characters
598  $count = strspn( $text, $curChar, $i );
599 
600  # we need to add to stack only if opening brace count is enough for one of the rules
601  if ( $count >= $rule['min'] ) {
602  # Add it to the stack
603  $piece = [
604  'open' => $curChar,
605  'close' => $rule['end'],
606  'count' => $count,
607  'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ),
608  ];
609 
610  $stack->push( $piece );
611  $accum =& $stack->getAccum();
612  $flags = $stack->getFlags();
613  extract( $flags );
614  } else {
615  # Add literal brace(s)
616  $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
617  }
618  $i += $count;
619  } elseif ( $found == 'close' ) {
620  $piece = $stack->top;
621  # lets check if there are enough characters for closing brace
622  $maxCount = $piece->count;
623  $count = strspn( $text, $curChar, $i, $maxCount );
624 
625  # check for maximum matching characters (if there are 5 closing
626  # characters, we will probably need only 3 - depending on the rules)
627  $rule = $this->rules[$piece->open];
628  if ( $count > $rule['max'] ) {
629  # The specified maximum exists in the callback array, unless the caller
630  # has made an error
631  $matchingCount = $rule['max'];
632  } else {
633  # Count is less than the maximum
634  # Skip any gaps in the callback array to find the true largest match
635  # Need to use array_key_exists not isset because the callback can be null
636  $matchingCount = $count;
637  while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
638  --$matchingCount;
639  }
640  }
641 
642  if ( $matchingCount <= 0 ) {
643  # No matching element found in callback array
644  # Output a literal closing brace and continue
645  $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
646  $i += $count;
647  continue;
648  }
649  $name = $rule['names'][$matchingCount];
650  if ( $name === null ) {
651  // No element, just literal text
652  $element = $piece->breakSyntax( $matchingCount ) . str_repeat( $rule['end'], $matchingCount );
653  } else {
654  # Create XML element
655  # Note: $parts is already XML, does not need to be encoded further
656  $parts = $piece->parts;
657  $title = $parts[0]->out;
658  unset( $parts[0] );
659 
660  # The invocation is at the start of the line if lineStart is set in
661  # the stack, and all opening brackets are used up.
662  if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
663  $attr = ' lineStart="1"';
664  } else {
665  $attr = '';
666  }
667 
668  $element = "<$name$attr>";
669  $element .= "<title>$title</title>";
670  $argIndex = 1;
671  foreach ( $parts as $part ) {
672  if ( isset( $part->eqpos ) ) {
673  $argName = substr( $part->out, 0, $part->eqpos );
674  $argValue = substr( $part->out, $part->eqpos + 1 );
675  $element .= "<part><name>$argName</name>=<value>$argValue</value></part>";
676  } else {
677  $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>";
678  $argIndex++;
679  }
680  }
681  $element .= "</$name>";
682  }
683 
684  # Advance input pointer
685  $i += $matchingCount;
686 
687  # Unwind the stack
688  $stack->pop();
689  $accum =& $stack->getAccum();
690 
691  # Re-add the old stack element if it still has unmatched opening characters remaining
692  if ( $matchingCount < $piece->count ) {
693  $piece->parts = [ new PPDPart ];
694  $piece->count -= $matchingCount;
695  # do we still qualify for any callback with remaining count?
696  $min = $this->rules[$piece->open]['min'];
697  if ( $piece->count >= $min ) {
698  $stack->push( $piece );
699  $accum =& $stack->getAccum();
700  } else {
701  $accum .= str_repeat( $piece->open, $piece->count );
702  }
703  }
704  $flags = $stack->getFlags();
705  extract( $flags );
706 
707  # Add XML element to the enclosing accumulator
708  $accum .= $element;
709  } elseif ( $found == 'pipe' ) {
710  $findEquals = true; // shortcut for getFlags()
711  $stack->addPart();
712  $accum =& $stack->getAccum();
713  ++$i;
714  } elseif ( $found == 'equals' ) {
715  $findEquals = false; // shortcut for getFlags()
716  $stack->getCurrentPart()->eqpos = strlen( $accum );
717  $accum .= '=';
718  ++$i;
719  }
720  }
721 
722  # Output any remaining unclosed brackets
723  foreach ( $stack->stack as $piece ) {
724  $stack->rootAccum .= $piece->breakSyntax();
725  }
726  $stack->rootAccum .= '</root>';
727  $xml = $stack->rootAccum;
728 
729  return $xml;
730  }
731 }
732 
737 class PPDStack {
739 
743  public $top;
744  public $out;
745  public $elementClass = 'PPDStackElement';
746 
747  public static $false = false;
748 
749  public function __construct() {
750  $this->stack = [];
751  $this->top = false;
752  $this->rootAccum = '';
753  $this->accum =& $this->rootAccum;
754  }
755 
759  public function count() {
760  return count( $this->stack );
761  }
762 
763  public function &getAccum() {
764  return $this->accum;
765  }
766 
767  public function getCurrentPart() {
768  if ( $this->top === false ) {
769  return false;
770  } else {
771  return $this->top->getCurrentPart();
772  }
773  }
774 
775  public function push( $data ) {
776  if ( $data instanceof $this->elementClass ) {
777  $this->stack[] = $data;
778  } else {
779  $class = $this->elementClass;
780  $this->stack[] = new $class( $data );
781  }
782  $this->top = $this->stack[count( $this->stack ) - 1];
783  $this->accum =& $this->top->getAccum();
784  }
785 
786  public function pop() {
787  if ( !count( $this->stack ) ) {
788  throw new MWException( __METHOD__ . ': no elements remaining' );
789  }
790  $temp = array_pop( $this->stack );
791 
792  if ( count( $this->stack ) ) {
793  $this->top = $this->stack[count( $this->stack ) - 1];
794  $this->accum =& $this->top->getAccum();
795  } else {
796  $this->top = self::$false;
797  $this->accum =& $this->rootAccum;
798  }
799  return $temp;
800  }
801 
802  public function addPart( $s = '' ) {
803  $this->top->addPart( $s );
804  $this->accum =& $this->top->getAccum();
805  }
806 
810  public function getFlags() {
811  if ( !count( $this->stack ) ) {
812  return [
813  'findEquals' => false,
814  'findPipe' => false,
815  'inHeading' => false,
816  ];
817  } else {
818  return $this->top->getFlags();
819  }
820  }
821 }
822 
830  public $open;
831 
835  public $close;
836 
840  public $count;
841 
845  public $parts;
846 
851  public $lineStart;
852 
853  public $partClass = 'PPDPart';
854 
855  public function __construct( $data = [] ) {
856  $class = $this->partClass;
857  $this->parts = [ new $class ];
858 
859  foreach ( $data as $name => $value ) {
860  $this->$name = $value;
861  }
862  }
863 
864  public function &getAccum() {
865  return $this->parts[count( $this->parts ) - 1]->out;
866  }
867 
868  public function addPart( $s = '' ) {
869  $class = $this->partClass;
870  $this->parts[] = new $class( $s );
871  }
872 
873  public function getCurrentPart() {
874  return $this->parts[count( $this->parts ) - 1];
875  }
876 
880  public function getFlags() {
881  $partCount = count( $this->parts );
882  $findPipe = $this->open != "\n" && $this->open != '[';
883  return [
884  'findPipe' => $findPipe,
885  'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ),
886  'inHeading' => $this->open == "\n",
887  ];
888  }
889 
896  public function breakSyntax( $openingCount = false ) {
897  if ( $this->open == "\n" ) {
898  $s = $this->parts[0]->out;
899  } else {
900  if ( $openingCount === false ) {
901  $openingCount = $this->count;
902  }
903  $s = str_repeat( $this->open, $openingCount );
904  $first = true;
905  foreach ( $this->parts as $part ) {
906  if ( $first ) {
907  $first = false;
908  } else {
909  $s .= '|';
910  }
911  $s .= $part->out;
912  }
913  }
914  return $s;
915  }
916 }
917 
921 class PPDPart {
925  public $out;
926 
927  // Optional member variables:
928  // eqpos Position of equals sign in output accumulator
929  // commentEnd Past-the-end input pointer for the last comment encountered
930  // visualEnd Past-the-end input pointer for the end of the accumulator minus comments
931 
932  public function __construct( $out = '' ) {
933  $this->out = $out;
934  }
935 }
936 
941 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
942 class PPFrame_DOM implements PPFrame {
943  // @codingStandardsIgnoreEnd
944 
949 
953  public $parser;
954 
958  public $title;
959  public $titleCache;
960 
966 
971  public $depth;
972 
973  private $volatile = false;
974  private $ttl = null;
975 
980 
985  public function __construct( $preprocessor ) {
986  $this->preprocessor = $preprocessor;
987  $this->parser = $preprocessor->parser;
988  $this->title = $this->parser->mTitle;
989  $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
990  $this->loopCheckHash = [];
991  $this->depth = 0;
992  $this->childExpansionCache = [];
993  }
994 
1004  public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
1005  $namedArgs = [];
1006  $numberedArgs = [];
1007  if ( $title === false ) {
1008  $title = $this->title;
1009  }
1010  if ( $args !== false ) {
1011  $xpath = false;
1012  if ( $args instanceof PPNode ) {
1013  $args = $args->node;
1014  }
1015  foreach ( $args as $arg ) {
1016  if ( $arg instanceof PPNode ) {
1017  $arg = $arg->node;
1018  }
1019  if ( !$xpath || $xpath->document !== $arg->ownerDocument ) {
1020  $xpath = new DOMXPath( $arg->ownerDocument );
1021  }
1022 
1023  $nameNodes = $xpath->query( 'name', $arg );
1024  $value = $xpath->query( 'value', $arg );
1025  if ( $nameNodes->item( 0 )->hasAttributes() ) {
1026  // Numbered parameter
1027  $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
1028  $index = $index - $indexOffset;
1029  if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
1030  $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
1031  wfEscapeWikiText( $this->title ),
1033  wfEscapeWikiText( $index ) )->text() );
1034  $this->parser->addTrackingCategory( 'duplicate-args-category' );
1035  }
1036  $numberedArgs[$index] = $value->item( 0 );
1037  unset( $namedArgs[$index] );
1038  } else {
1039  // Named parameter
1040  $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) );
1041  if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
1042  $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
1043  wfEscapeWikiText( $this->title ),
1045  wfEscapeWikiText( $name ) )->text() );
1046  $this->parser->addTrackingCategory( 'duplicate-args-category' );
1047  }
1048  $namedArgs[$name] = $value->item( 0 );
1049  unset( $numberedArgs[$name] );
1050  }
1051  }
1052  }
1053  return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
1054  }
1055 
1063  public function cachedExpand( $key, $root, $flags = 0 ) {
1064  // we don't have a parent, so we don't have a cache
1065  return $this->expand( $root, $flags );
1066  }
1067 
1074  public function expand( $root, $flags = 0 ) {
1075  static $expansionDepth = 0;
1076  if ( is_string( $root ) ) {
1077  return $root;
1078  }
1079 
1080  if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
1081  $this->parser->limitationWarn( 'node-count-exceeded',
1082  $this->parser->mPPNodeCount,
1083  $this->parser->mOptions->getMaxPPNodeCount()
1084  );
1085  return '<span class="error">Node-count limit exceeded</span>';
1086  }
1087 
1088  if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
1089  $this->parser->limitationWarn( 'expansion-depth-exceeded',
1090  $expansionDepth,
1091  $this->parser->mOptions->getMaxPPExpandDepth()
1092  );
1093  return '<span class="error">Expansion depth limit exceeded</span>';
1094  }
1095  ++$expansionDepth;
1096  if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
1097  $this->parser->mHighestExpansionDepth = $expansionDepth;
1098  }
1099 
1100  if ( $root instanceof PPNode_DOM ) {
1101  $root = $root->node;
1102  }
1103  if ( $root instanceof DOMDocument ) {
1104  $root = $root->documentElement;
1105  }
1106 
1107  $outStack = [ '', '' ];
1108  $iteratorStack = [ false, $root ];
1109  $indexStack = [ 0, 0 ];
1110 
1111  while ( count( $iteratorStack ) > 1 ) {
1112  $level = count( $outStack ) - 1;
1113  $iteratorNode =& $iteratorStack[$level];
1114  $out =& $outStack[$level];
1115  $index =& $indexStack[$level];
1116 
1117  if ( $iteratorNode instanceof PPNode_DOM ) {
1118  $iteratorNode = $iteratorNode->node;
1119  }
1120 
1121  if ( is_array( $iteratorNode ) ) {
1122  if ( $index >= count( $iteratorNode ) ) {
1123  // All done with this iterator
1124  $iteratorStack[$level] = false;
1125  $contextNode = false;
1126  } else {
1127  $contextNode = $iteratorNode[$index];
1128  $index++;
1129  }
1130  } elseif ( $iteratorNode instanceof DOMNodeList ) {
1131  if ( $index >= $iteratorNode->length ) {
1132  // All done with this iterator
1133  $iteratorStack[$level] = false;
1134  $contextNode = false;
1135  } else {
1136  $contextNode = $iteratorNode->item( $index );
1137  $index++;
1138  }
1139  } else {
1140  // Copy to $contextNode and then delete from iterator stack,
1141  // because this is not an iterator but we do have to execute it once
1142  $contextNode = $iteratorStack[$level];
1143  $iteratorStack[$level] = false;
1144  }
1145 
1146  if ( $contextNode instanceof PPNode_DOM ) {
1147  $contextNode = $contextNode->node;
1148  }
1149 
1150  $newIterator = false;
1151 
1152  if ( $contextNode === false ) {
1153  // nothing to do
1154  } elseif ( is_string( $contextNode ) ) {
1155  $out .= $contextNode;
1156  } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) {
1157  $newIterator = $contextNode;
1158  } elseif ( $contextNode instanceof DOMNode ) {
1159  if ( $contextNode->nodeType == XML_TEXT_NODE ) {
1160  $out .= $contextNode->nodeValue;
1161  } elseif ( $contextNode->nodeName == 'template' ) {
1162  # Double-brace expansion
1163  $xpath = new DOMXPath( $contextNode->ownerDocument );
1164  $titles = $xpath->query( 'title', $contextNode );
1165  $title = $titles->item( 0 );
1166  $parts = $xpath->query( 'part', $contextNode );
1167  if ( $flags & PPFrame::NO_TEMPLATES ) {
1168  $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
1169  } else {
1170  $lineStart = $contextNode->getAttribute( 'lineStart' );
1171  $params = [
1172  'title' => new PPNode_DOM( $title ),
1173  'parts' => new PPNode_DOM( $parts ),
1174  'lineStart' => $lineStart ];
1175  $ret = $this->parser->braceSubstitution( $params, $this );
1176  if ( isset( $ret['object'] ) ) {
1177  $newIterator = $ret['object'];
1178  } else {
1179  $out .= $ret['text'];
1180  }
1181  }
1182  } elseif ( $contextNode->nodeName == 'tplarg' ) {
1183  # Triple-brace expansion
1184  $xpath = new DOMXPath( $contextNode->ownerDocument );
1185  $titles = $xpath->query( 'title', $contextNode );
1186  $title = $titles->item( 0 );
1187  $parts = $xpath->query( 'part', $contextNode );
1188  if ( $flags & PPFrame::NO_ARGS ) {
1189  $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
1190  } else {
1191  $params = [
1192  'title' => new PPNode_DOM( $title ),
1193  'parts' => new PPNode_DOM( $parts ) ];
1194  $ret = $this->parser->argSubstitution( $params, $this );
1195  if ( isset( $ret['object'] ) ) {
1196  $newIterator = $ret['object'];
1197  } else {
1198  $out .= $ret['text'];
1199  }
1200  }
1201  } elseif ( $contextNode->nodeName == 'comment' ) {
1202  # HTML-style comment
1203  # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1204  # Not in RECOVER_COMMENTS mode (msgnw) though.
1205  if ( ( $this->parser->ot['html']
1206  || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1208  ) && !( $flags & PPFrame::RECOVER_COMMENTS )
1209  ) {
1210  $out .= '';
1211  } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1212  # Add a strip marker in PST mode so that pstPass2() can
1213  # run some old-fashioned regexes on the result.
1214  # Not in RECOVER_COMMENTS mode (extractSections) though.
1215  $out .= $this->parser->insertStripItem( $contextNode->textContent );
1216  } else {
1217  # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1218  $out .= $contextNode->textContent;
1219  }
1220  } elseif ( $contextNode->nodeName == 'ignore' ) {
1221  # Output suppression used by <includeonly> etc.
1222  # OT_WIKI will only respect <ignore> in substed templates.
1223  # The other output types respect it unless NO_IGNORE is set.
1224  # extractSections() sets NO_IGNORE and so never respects it.
1225  if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1226  || ( $flags & PPFrame::NO_IGNORE )
1227  ) {
1228  $out .= $contextNode->textContent;
1229  } else {
1230  $out .= '';
1231  }
1232  } elseif ( $contextNode->nodeName == 'ext' ) {
1233  # Extension tag
1234  $xpath = new DOMXPath( $contextNode->ownerDocument );
1235  $names = $xpath->query( 'name', $contextNode );
1236  $attrs = $xpath->query( 'attr', $contextNode );
1237  $inners = $xpath->query( 'inner', $contextNode );
1238  $closes = $xpath->query( 'close', $contextNode );
1239  if ( $flags & PPFrame::NO_TAGS ) {
1240  $s = '<' . $this->expand( $names->item( 0 ), $flags );
1241  if ( $attrs->length > 0 ) {
1242  $s .= $this->expand( $attrs->item( 0 ), $flags );
1243  }
1244  if ( $inners->length > 0 ) {
1245  $s .= '>' . $this->expand( $inners->item( 0 ), $flags );
1246  if ( $closes->length > 0 ) {
1247  $s .= $this->expand( $closes->item( 0 ), $flags );
1248  }
1249  } else {
1250  $s .= '/>';
1251  }
1252  $out .= $s;
1253  } else {
1254  $params = [
1255  'name' => new PPNode_DOM( $names->item( 0 ) ),
1256  'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null,
1257  'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null,
1258  'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null,
1259  ];
1260  $out .= $this->parser->extensionSubstitution( $params, $this );
1261  }
1262  } elseif ( $contextNode->nodeName == 'h' ) {
1263  # Heading
1264  $s = $this->expand( $contextNode->childNodes, $flags );
1265 
1266  # Insert a heading marker only for <h> children of <root>
1267  # This is to stop extractSections from going over multiple tree levels
1268  if ( $contextNode->parentNode->nodeName == 'root' && $this->parser->ot['html'] ) {
1269  # Insert heading index marker
1270  $headingIndex = $contextNode->getAttribute( 'i' );
1271  $titleText = $this->title->getPrefixedDBkey();
1272  $this->parser->mHeadings[] = [ $titleText, $headingIndex ];
1273  $serial = count( $this->parser->mHeadings ) - 1;
1274  $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
1275  $count = $contextNode->getAttribute( 'level' );
1276  $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
1277  $this->parser->mStripState->addGeneral( $marker, '' );
1278  }
1279  $out .= $s;
1280  } else {
1281  # Generic recursive expansion
1282  $newIterator = $contextNode->childNodes;
1283  }
1284  } else {
1285  throw new MWException( __METHOD__ . ': Invalid parameter type' );
1286  }
1287 
1288  if ( $newIterator !== false ) {
1289  if ( $newIterator instanceof PPNode_DOM ) {
1290  $newIterator = $newIterator->node;
1291  }
1292  $outStack[] = '';
1293  $iteratorStack[] = $newIterator;
1294  $indexStack[] = 0;
1295  } elseif ( $iteratorStack[$level] === false ) {
1296  // Return accumulated value to parent
1297  // With tail recursion
1298  while ( $iteratorStack[$level] === false && $level > 0 ) {
1299  $outStack[$level - 1] .= $out;
1300  array_pop( $outStack );
1301  array_pop( $iteratorStack );
1302  array_pop( $indexStack );
1303  $level--;
1304  }
1305  }
1306  }
1307  --$expansionDepth;
1308  return $outStack[0];
1309  }
1310 
1317  public function implodeWithFlags( $sep, $flags /*, ... */ ) {
1318  $args = array_slice( func_get_args(), 2 );
1319 
1320  $first = true;
1321  $s = '';
1322  foreach ( $args as $root ) {
1323  if ( $root instanceof PPNode_DOM ) {
1324  $root = $root->node;
1325  }
1326  if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1327  $root = [ $root ];
1328  }
1329  foreach ( $root as $node ) {
1330  if ( $first ) {
1331  $first = false;
1332  } else {
1333  $s .= $sep;
1334  }
1335  $s .= $this->expand( $node, $flags );
1336  }
1337  }
1338  return $s;
1339  }
1340 
1349  public function implode( $sep /*, ... */ ) {
1350  $args = array_slice( func_get_args(), 1 );
1351 
1352  $first = true;
1353  $s = '';
1354  foreach ( $args as $root ) {
1355  if ( $root instanceof PPNode_DOM ) {
1356  $root = $root->node;
1357  }
1358  if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1359  $root = [ $root ];
1360  }
1361  foreach ( $root as $node ) {
1362  if ( $first ) {
1363  $first = false;
1364  } else {
1365  $s .= $sep;
1366  }
1367  $s .= $this->expand( $node );
1368  }
1369  }
1370  return $s;
1371  }
1372 
1381  public function virtualImplode( $sep /*, ... */ ) {
1382  $args = array_slice( func_get_args(), 1 );
1383  $out = [];
1384  $first = true;
1385 
1386  foreach ( $args as $root ) {
1387  if ( $root instanceof PPNode_DOM ) {
1388  $root = $root->node;
1389  }
1390  if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1391  $root = [ $root ];
1392  }
1393  foreach ( $root as $node ) {
1394  if ( $first ) {
1395  $first = false;
1396  } else {
1397  $out[] = $sep;
1398  }
1399  $out[] = $node;
1400  }
1401  }
1402  return $out;
1403  }
1404 
1413  public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1414  $args = array_slice( func_get_args(), 3 );
1415  $out = [ $start ];
1416  $first = true;
1417 
1418  foreach ( $args as $root ) {
1419  if ( $root instanceof PPNode_DOM ) {
1420  $root = $root->node;
1421  }
1422  if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1423  $root = [ $root ];
1424  }
1425  foreach ( $root as $node ) {
1426  if ( $first ) {
1427  $first = false;
1428  } else {
1429  $out[] = $sep;
1430  }
1431  $out[] = $node;
1432  }
1433  }
1434  $out[] = $end;
1435  return $out;
1436  }
1437 
1438  public function __toString() {
1439  return 'frame{}';
1440  }
1441 
1442  public function getPDBK( $level = false ) {
1443  if ( $level === false ) {
1444  return $this->title->getPrefixedDBkey();
1445  } else {
1446  return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
1447  }
1448  }
1449 
1453  public function getArguments() {
1454  return [];
1455  }
1456 
1460  public function getNumberedArguments() {
1461  return [];
1462  }
1463 
1467  public function getNamedArguments() {
1468  return [];
1469  }
1470 
1476  public function isEmpty() {
1477  return true;
1478  }
1479 
1484  public function getArgument( $name ) {
1485  return false;
1486  }
1487 
1494  public function loopCheck( $title ) {
1495  return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1496  }
1497 
1503  public function isTemplate() {
1504  return false;
1505  }
1506 
1512  public function getTitle() {
1513  return $this->title;
1514  }
1515 
1521  public function setVolatile( $flag = true ) {
1522  $this->volatile = $flag;
1523  }
1524 
1530  public function isVolatile() {
1531  return $this->volatile;
1532  }
1533 
1539  public function setTTL( $ttl ) {
1540  if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1541  $this->ttl = $ttl;
1542  }
1543  }
1544 
1550  public function getTTL() {
1551  return $this->ttl;
1552  }
1553 }
1554 
1559 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1561  // @codingStandardsIgnoreEnd
1562 
1564 
1568  public $parent;
1570 
1578  public function __construct( $preprocessor, $parent = false, $numberedArgs = [],
1579  $namedArgs = [], $title = false
1580  ) {
1581  parent::__construct( $preprocessor );
1582 
1583  $this->parent = $parent;
1584  $this->numberedArgs = $numberedArgs;
1585  $this->namedArgs = $namedArgs;
1586  $this->title = $title;
1587  $pdbk = $title ? $title->getPrefixedDBkey() : false;
1588  $this->titleCache = $parent->titleCache;
1589  $this->titleCache[] = $pdbk;
1590  $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1591  if ( $pdbk !== false ) {
1592  $this->loopCheckHash[$pdbk] = true;
1593  }
1594  $this->depth = $parent->depth + 1;
1595  $this->numberedExpansionCache = $this->namedExpansionCache = [];
1596  }
1597 
1598  public function __toString() {
1599  $s = 'tplframe{';
1600  $first = true;
1601  $args = $this->numberedArgs + $this->namedArgs;
1602  foreach ( $args as $name => $value ) {
1603  if ( $first ) {
1604  $first = false;
1605  } else {
1606  $s .= ', ';
1607  }
1608  $s .= "\"$name\":\"" .
1609  str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"';
1610  }
1611  $s .= '}';
1612  return $s;
1613  }
1614 
1622  public function cachedExpand( $key, $root, $flags = 0 ) {
1623  if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1624  return $this->parent->childExpansionCache[$key];
1625  }
1626  $retval = $this->expand( $root, $flags );
1627  if ( !$this->isVolatile() ) {
1628  $this->parent->childExpansionCache[$key] = $retval;
1629  }
1630  return $retval;
1631  }
1632 
1638  public function isEmpty() {
1639  return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1640  }
1641 
1642  public function getArguments() {
1643  $arguments = [];
1644  foreach ( array_merge(
1645  array_keys( $this->numberedArgs ),
1646  array_keys( $this->namedArgs ) ) as $key ) {
1647  $arguments[$key] = $this->getArgument( $key );
1648  }
1649  return $arguments;
1650  }
1651 
1652  public function getNumberedArguments() {
1653  $arguments = [];
1654  foreach ( array_keys( $this->numberedArgs ) as $key ) {
1655  $arguments[$key] = $this->getArgument( $key );
1656  }
1657  return $arguments;
1658  }
1659 
1660  public function getNamedArguments() {
1661  $arguments = [];
1662  foreach ( array_keys( $this->namedArgs ) as $key ) {
1663  $arguments[$key] = $this->getArgument( $key );
1664  }
1665  return $arguments;
1666  }
1667 
1672  public function getNumberedArgument( $index ) {
1673  if ( !isset( $this->numberedArgs[$index] ) ) {
1674  return false;
1675  }
1676  if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1677  # No trimming for unnamed arguments
1678  $this->numberedExpansionCache[$index] = $this->parent->expand(
1679  $this->numberedArgs[$index],
1681  );
1682  }
1683  return $this->numberedExpansionCache[$index];
1684  }
1685 
1690  public function getNamedArgument( $name ) {
1691  if ( !isset( $this->namedArgs[$name] ) ) {
1692  return false;
1693  }
1694  if ( !isset( $this->namedExpansionCache[$name] ) ) {
1695  # Trim named arguments post-expand, for backwards compatibility
1696  $this->namedExpansionCache[$name] = trim(
1697  $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1698  }
1699  return $this->namedExpansionCache[$name];
1700  }
1701 
1706  public function getArgument( $name ) {
1707  $text = $this->getNumberedArgument( $name );
1708  if ( $text === false ) {
1709  $text = $this->getNamedArgument( $name );
1710  }
1711  return $text;
1712  }
1713 
1719  public function isTemplate() {
1720  return true;
1721  }
1722 
1723  public function setVolatile( $flag = true ) {
1724  parent::setVolatile( $flag );
1725  $this->parent->setVolatile( $flag );
1726  }
1727 
1728  public function setTTL( $ttl ) {
1729  parent::setTTL( $ttl );
1730  $this->parent->setTTL( $ttl );
1731  }
1732 }
1733 
1738 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1740  // @codingStandardsIgnoreEnd
1741 
1742  public $args;
1743 
1744  public function __construct( $preprocessor, $args ) {
1745  parent::__construct( $preprocessor );
1746  $this->args = $args;
1747  }
1748 
1749  public function __toString() {
1750  $s = 'cstmframe{';
1751  $first = true;
1752  foreach ( $this->args as $name => $value ) {
1753  if ( $first ) {
1754  $first = false;
1755  } else {
1756  $s .= ', ';
1757  }
1758  $s .= "\"$name\":\"" .
1759  str_replace( '"', '\\"', $value->__toString() ) . '"';
1760  }
1761  $s .= '}';
1762  return $s;
1763  }
1764 
1768  public function isEmpty() {
1769  return !count( $this->args );
1770  }
1771 
1776  public function getArgument( $index ) {
1777  if ( !isset( $this->args[$index] ) ) {
1778  return false;
1779  }
1780  return $this->args[$index];
1781  }
1782 
1783  public function getArguments() {
1784  return $this->args;
1785  }
1786 }
1787 
1791 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1792 class PPNode_DOM implements PPNode {
1793  // @codingStandardsIgnoreEnd
1794 
1798  public $node;
1799  public $xpath;
1800 
1801  public function __construct( $node, $xpath = false ) {
1802  $this->node = $node;
1803  }
1804 
1808  public function getXPath() {
1809  if ( $this->xpath === null ) {
1810  $this->xpath = new DOMXPath( $this->node->ownerDocument );
1811  }
1812  return $this->xpath;
1813  }
1814 
1815  public function __toString() {
1816  if ( $this->node instanceof DOMNodeList ) {
1817  $s = '';
1818  foreach ( $this->node as $node ) {
1819  $s .= $node->ownerDocument->saveXML( $node );
1820  }
1821  } else {
1822  $s = $this->node->ownerDocument->saveXML( $this->node );
1823  }
1824  return $s;
1825  }
1826 
1830  public function getChildren() {
1831  return $this->node->childNodes ? new self( $this->node->childNodes ) : false;
1832  }
1833 
1837  public function getFirstChild() {
1838  return $this->node->firstChild ? new self( $this->node->firstChild ) : false;
1839  }
1840 
1844  public function getNextSibling() {
1845  return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false;
1846  }
1847 
1853  public function getChildrenOfType( $type ) {
1854  return new self( $this->getXPath()->query( $type, $this->node ) );
1855  }
1856 
1860  public function getLength() {
1861  if ( $this->node instanceof DOMNodeList ) {
1862  return $this->node->length;
1863  } else {
1864  return false;
1865  }
1866  }
1867 
1872  public function item( $i ) {
1873  $item = $this->node->item( $i );
1874  return $item ? new self( $item ) : false;
1875  }
1876 
1880  public function getName() {
1881  if ( $this->node instanceof DOMNodeList ) {
1882  return '#nodelist';
1883  } else {
1884  return $this->node->nodeName;
1885  }
1886  }
1887 
1897  public function splitArg() {
1898  $xpath = $this->getXPath();
1899  $names = $xpath->query( 'name', $this->node );
1900  $values = $xpath->query( 'value', $this->node );
1901  if ( !$names->length || !$values->length ) {
1902  throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
1903  }
1904  $name = $names->item( 0 );
1905  $index = $name->getAttribute( 'index' );
1906  return [
1907  'name' => new self( $name ),
1908  'index' => $index,
1909  'value' => new self( $values->item( 0 ) ) ];
1910  }
1911 
1919  public function splitExt() {
1920  $xpath = $this->getXPath();
1921  $names = $xpath->query( 'name', $this->node );
1922  $attrs = $xpath->query( 'attr', $this->node );
1923  $inners = $xpath->query( 'inner', $this->node );
1924  $closes = $xpath->query( 'close', $this->node );
1925  if ( !$names->length || !$attrs->length ) {
1926  throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
1927  }
1928  $parts = [
1929  'name' => new self( $names->item( 0 ) ),
1930  'attr' => new self( $attrs->item( 0 ) ) ];
1931  if ( $inners->length ) {
1932  $parts['inner'] = new self( $inners->item( 0 ) );
1933  }
1934  if ( $closes->length ) {
1935  $parts['close'] = new self( $closes->item( 0 ) );
1936  }
1937  return $parts;
1938  }
1939 
1945  public function splitHeading() {
1946  if ( $this->getName() !== 'h' ) {
1947  throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1948  }
1949  return [
1950  'i' => $this->node->getAttribute( 'i' ),
1951  'level' => $this->node->getAttribute( 'level' ),
1952  'contents' => $this->getChildren()
1953  ];
1954  }
1955 }
preprocessToXml($text, $flags=0)
bool $lineStart
True if the open char appeared at the start of the input line.
const MARKER_PREFIX
Definition: Parser.php:141
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:762
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2321
Stack class to help Preprocessor::preprocessToObj()
getTTL()
Get the TTL.
loopCheck($title)
Returns true if the infinite loop check is OK, false if a loop is detected.
DOMElement $node
int $count
Number of opening characters found (number of "=" for heading)
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1798
__construct($out= '')
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
Expansion frame with template arguments.
There are three types of nodes:
$value
getTitle()
Get a title of frame.
getArguments()
Returns all arguments of this frame.
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2548
virtualBracketedImplode($start, $sep, $end)
Virtual implode with brackets.
PPDStack $top
const RECOVER_COMMENTS
const NO_ARGS
const NO_IGNORE
cachedExpand($key, $root, $flags=0)
getArguments()
Returns all arguments of this frame.
expand($root, $flags=0)
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 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload.Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports.&$fullInterwikiPrefix:Interwiki prefix, may contain colons.&$pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable.Can be used to lazy-load the import sources list.&$importSources:The value of $wgImportSources.Modify as necessary.See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page.$context:IContextSource object &$pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect.&$title:Title object for the current page &$request:WebRequest &$ignoreRedirect:boolean to skip redirect check &$target:Title/string of redirect target &$article:Article object 'InternalParseBeforeLinks':during Parser's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InternalParseBeforeSanitize':during Parser's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings.Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not.Return true without providing an interwiki to continue interwiki search.$prefix:interwiki prefix we are looking for.&$iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user's email has been invalidated successfully.$user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification.Callee may modify $url and $query, URL will be constructed as $url.$query &$url:URL to index.php &$query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) &$article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() &$ip:IP being check &$result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from &$allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn't match your organization.$addr:The e-mail address entered by the user &$result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user &$result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we're looking for a messages file for &$file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED!Use $magicWords in a file listed in $wgExtensionMessagesFiles instead.Use this to define synonyms of magic words depending of the language &$magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces.Do not use this hook to add namespaces.Use CanonicalNamespaces for that.&$namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED!Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead.Use to define aliases of special pages names depending of the language &$specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names.&$names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page's language links.This is called in various places to allow extensions to define the effective language links for a page.$title:The page's Title.&$links:Associative array mapping language codes to prefixed links of the form"language:title".&$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':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:1796
isTemplate()
Return true if the frame is a template frame.
if($line===false) $args
Definition: cdb.php:64
getPDBK($level=false)
const NO_TEMPLATES
array cacheSetTree($text, $flags, $tree)
Store a document tree in the cache.
isTemplate()
Return true if the frame is a template frame.
splitHeading()
Split a "" node.
wfEscapeWikiText($text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
string $open
Opening character (\n for heading)
__construct($preprocessor)
Construct a new preprocessor frame.
implodeWithFlags($sep, $flags)
isEmpty()
Returns true if there are no arguments in this frame.
setTTL($ttl)
Set the TTL of the output of this frame and all of its ancestors.
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock()-offset Set to overwrite offset parameter in $wgRequest set to ''to unsetoffset-wrap String Wrap the message in html(usually something like"&lt
$params
Expansion frame with custom arguments.
newChild($args=false, $title=false, $indexOffset=0)
Create a new child frame $args is optionally a multi-root PPNode or array containing the template arg...
implode($sep)
Implode with no flags specified This previously called implodeWithFlags but has now been inlined to r...
title
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:912
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition: design.txt:12
Preprocessor $preprocessor
string $close
Matching closing character.
An expansion frame, used as a context to expand the result of preprocessToObj()
setTTL($ttl)
Set the TTL.
const PTD_FOR_INCLUSION
Definition: Parser.php:113
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
$loopCheckHash
Hashtable listing templates which are disallowed for expansion in this frame, having been encountered...
preprocessToObj($text, $flags=0)
Preprocess some wikitext and return the document tree.
__construct($preprocessor, $args)
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
__construct($node, $xpath=false)
const NO_TAGS
cachedExpand($key, $root, $flags=0)
addPart($s= '')
linkcache txt The LinkCache class maintains a list of article titles and the information about whether or not the article exists in the database This is used to mark up links when displaying a page If the same link appears more than once on any page then it only has to be looked up once In most cases link lookups are done in batches with the LinkBatch class or the equivalent in so the link cache is mostly useful for short snippets of parsed and for links in the navigation areas of the skin The link cache was formerly used to track links used in a document for the purposes of updating the link tables This application is now deprecated To create a you can use the following $titles
Definition: linkcache.txt:17
setVolatile($flag=true)
Set the "volatile" flag.
isVolatile()
Get the volatile flag.
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context the output can only depend on parameters provided to this hook not on global state indicating whether full HTML should be generated If generation of HTML may be but other information should still be present in the ParserOutput object to manipulate or replace but no entry for that model exists in $wgContentHandlers if desired whether it is OK to use $contentModel on $title Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok inclusive $limit
Definition: hooks.txt:1004
array $childExpansionCache
setVolatile($flag=true)
Set the volatile flag.
$count
$depth
Recursion depth of this frame, top = 0 Note that this is NOT the same as expansion depth in expand() ...
splitArg()
Split a "" node into an associative array containing:
getNamedArguments()
Returns all named arguments of this frame.
PPDPart[] $parts
Array of PPDPart objects describing pipe-separated parts.
isEmpty()
Returns true if there are no arguments in this frame.
__construct($preprocessor, $parent=false, $numberedArgs=[], $namedArgs=[], $title=false)
const STRIP_COMMENTS
virtualImplode($sep)
Makes an object that, when expand()ed, will be the same as one obtained with implode() ...
splitExt()
Split an "" node into an associative array containing name, attr, inner and close All values in ...
breakSyntax($openingCount=false)
Get the output string that would result if the close is not found.
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account incomplete not yet checked for validity & $retval
Definition: hooks.txt:242
string $out
Output accumulator string.
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached one of or reset my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition: hooks.txt:2338
getChildrenOfType($type)
getNumberedArguments()
Returns all numbered arguments of this frame.
cacheGetTree($text, $flags)
Attempt to load a precomputed document tree for some given wikitext from the cache.
getPrefixedDBkey()
Get the prefixed database key form.
Definition: Title.php:1437
$matches
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:310