MediaWiki  master
Preprocessor_Hash.php
Go to the documentation of this file.
1 <?php
42 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
44 
48  public $parser;
49 
50  const CACHE_PREFIX = 'preprocess-hash';
51  const CACHE_VERSION = 2;
52 
53  public function __construct( $parser ) {
54  $this->parser = $parser;
55  }
56 
60  public function newFrame() {
61  return new PPFrame_Hash( $this );
62  }
63 
68  public function newCustomFrame( $args ) {
69  return new PPCustomFrame_Hash( $this, $args );
70  }
71 
76  public function newPartNodeArray( $values ) {
77  $list = [];
78 
79  foreach ( $values as $k => $val ) {
80  if ( is_int( $k ) ) {
81  $store = [ [ 'part', [
82  [ 'name', [ [ '@index', [ $k ] ] ] ],
83  [ 'value', [ strval( $val ) ] ],
84  ] ] ];
85  } else {
86  $store = [ [ 'part', [
87  [ 'name', [ strval( $k ) ] ],
88  '=',
89  [ 'value', [ strval( $val ) ] ],
90  ] ] ];
91  }
92 
93  $list[] = new PPNode_Hash_Tree( $store, 0 );
94  }
95 
96  $node = new PPNode_Hash_Array( $list );
97  return $node;
98  }
99 
118  public function preprocessToObj( $text, $flags = 0 ) {
120 
121  $tree = $this->cacheGetTree( $text, $flags );
122  if ( $tree !== false ) {
123  $store = json_decode( $tree );
124  if ( is_array( $store ) ) {
125  return new PPNode_Hash_Tree( $store, 0 );
126  }
127  }
128 
129  $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
130 
131  $xmlishElements = $this->parser->getStripList();
132  $xmlishAllowMissingEndTag = [ 'includeonly', 'noinclude', 'onlyinclude' ];
133  $enableOnlyinclude = false;
134  if ( $forInclusion ) {
135  $ignoredTags = [ 'includeonly', '/includeonly' ];
136  $ignoredElements = [ 'noinclude' ];
137  $xmlishElements[] = 'noinclude';
138  if ( strpos( $text, '<onlyinclude>' ) !== false
139  && strpos( $text, '</onlyinclude>' ) !== false
140  ) {
141  $enableOnlyinclude = true;
142  }
143  } else {
144  $ignoredTags = [ 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ];
145  $ignoredElements = [ 'includeonly' ];
146  $xmlishElements[] = 'includeonly';
147  }
148  $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
149 
150  // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
151  $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
152 
153  $stack = new PPDStack_Hash;
154 
155  $searchBase = "[{<\n";
156  if ( !$wgDisableLangConversion ) {
157  $searchBase .= '-';
158  }
159 
160  // For fast reverse searches
161  $revText = strrev( $text );
162  $lengthText = strlen( $text );
163 
164  // Input pointer, starts out pointing to a pseudo-newline before the start
165  $i = 0;
166  // Current accumulator. See the doc comment for Preprocessor_Hash for the format.
167  $accum =& $stack->getAccum();
168  // True to find equals signs in arguments
169  $findEquals = false;
170  // True to take notice of pipe characters
171  $findPipe = false;
172  $headingIndex = 1;
173  // True if $i is inside a possible heading
174  $inHeading = false;
175  // True if there are no more greater-than (>) signs right of $i
176  $noMoreGT = false;
177  // Map of tag name => true if there are no more closing tags of given type right of $i
178  $noMoreClosingTag = [];
179  // True to ignore all input up to the next <onlyinclude>
180  $findOnlyinclude = $enableOnlyinclude;
181  // Do a line-start run without outputting an LF character
182  $fakeLineStart = true;
183 
184  while ( true ) {
185  // $this->memCheck();
186 
187  if ( $findOnlyinclude ) {
188  // Ignore all input up to the next <onlyinclude>
189  $startPos = strpos( $text, '<onlyinclude>', $i );
190  if ( $startPos === false ) {
191  // Ignored section runs to the end
192  $accum[] = [ 'ignore', [ substr( $text, $i ) ] ];
193  break;
194  }
195  $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
196  $accum[] = [ 'ignore', [ substr( $text, $i, $tagEndPos - $i ) ] ];
197  $i = $tagEndPos;
198  $findOnlyinclude = false;
199  }
200 
201  if ( $fakeLineStart ) {
202  $found = 'line-start';
203  $curChar = '';
204  } else {
205  # Find next opening brace, closing brace or pipe
206  $search = $searchBase;
207  if ( $stack->top === false ) {
208  $currentClosing = '';
209  } else {
210  $currentClosing = $stack->top->close;
211  $search .= $currentClosing;
212  }
213  if ( $findPipe ) {
214  $search .= '|';
215  }
216  if ( $findEquals ) {
217  // First equals will be for the template
218  $search .= '=';
219  }
220  $rule = null;
221  # Output literal section, advance input counter
222  $literalLength = strcspn( $text, $search, $i );
223  if ( $literalLength > 0 ) {
224  self::addLiteral( $accum, substr( $text, $i, $literalLength ) );
225  $i += $literalLength;
226  }
227  if ( $i >= $lengthText ) {
228  if ( $currentClosing == "\n" ) {
229  // Do a past-the-end run to finish off the heading
230  $curChar = '';
231  $found = 'line-end';
232  } else {
233  # All done
234  break;
235  }
236  } else {
237  $curChar = $curTwoChar = $text[$i];
238  if ( ( $i + 1 ) < $lengthText ) {
239  $curTwoChar .= $text[$i + 1];
240  }
241  if ( $curChar == '|' ) {
242  $found = 'pipe';
243  } elseif ( $curChar == '=' ) {
244  $found = 'equals';
245  } elseif ( $curChar == '<' ) {
246  $found = 'angle';
247  } elseif ( $curChar == "\n" ) {
248  if ( $inHeading ) {
249  $found = 'line-end';
250  } else {
251  $found = 'line-start';
252  }
253  } elseif ( $curTwoChar == $currentClosing ) {
254  $found = 'close';
255  $curChar = $curTwoChar;
256  } elseif ( $curChar == $currentClosing ) {
257  $found = 'close';
258  } elseif ( isset( $this->rules[$curTwoChar] ) ) {
259  $curChar = $curTwoChar;
260  $found = 'open';
261  $rule = $this->rules[$curChar];
262  } elseif ( isset( $this->rules[$curChar] ) ) {
263  $found = 'open';
264  $rule = $this->rules[$curChar];
265  } else {
266  # Some versions of PHP have a strcspn which stops on
267  # null characters; ignore these and continue.
268  # We also may get '-' and '}' characters here which
269  # don't match -{ or $currentClosing. Add these to
270  # output and continue.
271  if ( $curChar == '-' || $curChar == '}' ) {
272  self::addLiteral( $accum, $curChar );
273  }
274  ++$i;
275  continue;
276  }
277  }
278  }
279 
280  if ( $found == 'angle' ) {
281  $matches = false;
282  // Handle </onlyinclude>
283  if ( $enableOnlyinclude
284  && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>'
285  ) {
286  $findOnlyinclude = true;
287  continue;
288  }
289 
290  // Determine element name
291  if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
292  // Element name missing or not listed
293  self::addLiteral( $accum, '<' );
294  ++$i;
295  continue;
296  }
297  // Handle comments
298  if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
299  // To avoid leaving blank lines, when a sequence of
300  // space-separated comments is both preceded and followed by
301  // a newline (ignoring spaces), then
302  // trim leading and trailing spaces and the trailing newline.
303 
304  // Find the end
305  $endPos = strpos( $text, '-->', $i + 4 );
306  if ( $endPos === false ) {
307  // Unclosed comment in input, runs to end
308  $inner = substr( $text, $i );
309  $accum[] = [ 'comment', [ $inner ] ];
310  $i = $lengthText;
311  } else {
312  // Search backwards for leading whitespace
313  $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0;
314 
315  // Search forwards for trailing whitespace
316  // $wsEnd will be the position of the last space (or the '>' if there's none)
317  $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 );
318 
319  // Keep looking forward as long as we're finding more
320  // comments.
321  $comments = [ [ $wsStart, $wsEnd ] ];
322  while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) {
323  $c = strpos( $text, '-->', $wsEnd + 4 );
324  if ( $c === false ) {
325  break;
326  }
327  $c = $c + 2 + strspn( $text, " \t", $c + 3 );
328  $comments[] = [ $wsEnd + 1, $c ];
329  $wsEnd = $c;
330  }
331 
332  // Eat the line if possible
333  // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
334  // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
335  // it's a possible beneficial b/c break.
336  if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
337  && substr( $text, $wsEnd + 1, 1 ) == "\n"
338  ) {
339  // Remove leading whitespace from the end of the accumulator
340  $wsLength = $i - $wsStart;
341  $endIndex = count( $accum ) - 1;
342 
343  // Sanity check
344  if ( $wsLength > 0
345  && $endIndex >= 0
346  && is_string( $accum[$endIndex] )
347  && strspn( $accum[$endIndex], " \t", -$wsLength ) === $wsLength
348  ) {
349  $accum[$endIndex] = substr( $accum[$endIndex], 0, -$wsLength );
350  }
351 
352  // Dump all but the last comment to the accumulator
353  foreach ( $comments as $j => $com ) {
354  $startPos = $com[0];
355  $endPos = $com[1] + 1;
356  if ( $j == ( count( $comments ) - 1 ) ) {
357  break;
358  }
359  $inner = substr( $text, $startPos, $endPos - $startPos );
360  $accum[] = [ 'comment', [ $inner ] ];
361  }
362 
363  // Do a line-start run next time to look for headings after the comment
364  $fakeLineStart = true;
365  } else {
366  // No line to eat, just take the comment itself
367  $startPos = $i;
368  $endPos += 2;
369  }
370 
371  if ( $stack->top ) {
372  $part = $stack->top->getCurrentPart();
373  if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) {
374  $part->visualEnd = $wsStart;
375  }
376  // Else comments abutting, no change in visual end
377  $part->commentEnd = $endPos;
378  }
379  $i = $endPos + 1;
380  $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
381  $accum[] = [ 'comment', [ $inner ] ];
382  }
383  continue;
384  }
385  $name = $matches[1];
386  $lowerName = strtolower( $name );
387  $attrStart = $i + strlen( $name ) + 1;
388 
389  // Find end of tag
390  $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
391  if ( $tagEndPos === false ) {
392  // Infinite backtrack
393  // Disable tag search to prevent worst-case O(N^2) performance
394  $noMoreGT = true;
395  self::addLiteral( $accum, '<' );
396  ++$i;
397  continue;
398  }
399 
400  // Handle ignored tags
401  if ( in_array( $lowerName, $ignoredTags ) ) {
402  $accum[] = [ 'ignore', [ substr( $text, $i, $tagEndPos - $i + 1 ) ] ];
403  $i = $tagEndPos + 1;
404  continue;
405  }
406 
407  $tagStartPos = $i;
408  if ( $text[$tagEndPos - 1] == '/' ) {
409  // Short end tag
410  $attrEnd = $tagEndPos - 1;
411  $inner = null;
412  $i = $tagEndPos + 1;
413  $close = null;
414  } else {
415  $attrEnd = $tagEndPos;
416  // Find closing tag
417  if (
418  !isset( $noMoreClosingTag[$name] ) &&
419  preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
420  $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 )
421  ) {
422  $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
423  $i = $matches[0][1] + strlen( $matches[0][0] );
424  $close = $matches[0][0];
425  } else {
426  // No end tag
427  if ( in_array( $name, $xmlishAllowMissingEndTag ) ) {
428  // Let it run out to the end of the text.
429  $inner = substr( $text, $tagEndPos + 1 );
430  $i = $lengthText;
431  $close = null;
432  } else {
433  // Don't match the tag, treat opening tag as literal and resume parsing.
434  $i = $tagEndPos + 1;
435  self::addLiteral( $accum,
436  substr( $text, $tagStartPos, $tagEndPos + 1 - $tagStartPos ) );
437  // Cache results, otherwise we have O(N^2) performance for input like <foo><foo><foo>...
438  $noMoreClosingTag[$name] = true;
439  continue;
440  }
441  }
442  }
443  // <includeonly> and <noinclude> just become <ignore> tags
444  if ( in_array( $lowerName, $ignoredElements ) ) {
445  $accum[] = [ 'ignore', [ substr( $text, $tagStartPos, $i - $tagStartPos ) ] ];
446  continue;
447  }
448 
449  if ( $attrEnd <= $attrStart ) {
450  $attr = '';
451  } else {
452  // Note that the attr element contains the whitespace between name and attribute,
453  // this is necessary for precise reconstruction during pre-save transform.
454  $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
455  }
456 
457  $children = [
458  [ 'name', [ $name ] ],
459  [ 'attr', [ $attr ] ] ];
460  if ( $inner !== null ) {
461  $children[] = [ 'inner', [ $inner ] ];
462  }
463  if ( $close !== null ) {
464  $children[] = [ 'close', [ $close ] ];
465  }
466  $accum[] = [ 'ext', $children ];
467  } elseif ( $found == 'line-start' ) {
468  // Is this the start of a heading?
469  // Line break belongs before the heading element in any case
470  if ( $fakeLineStart ) {
471  $fakeLineStart = false;
472  } else {
473  self::addLiteral( $accum, $curChar );
474  $i++;
475  }
476 
477  $count = strspn( $text, '=', $i, 6 );
478  if ( $count == 1 && $findEquals ) {
479  // DWIM: This looks kind of like a name/value separator.
480  // Let's let the equals handler have it and break the potential
481  // heading. This is heuristic, but AFAICT the methods for
482  // completely correct disambiguation are very complex.
483  } elseif ( $count > 0 ) {
484  $piece = [
485  'open' => "\n",
486  'close' => "\n",
487  'parts' => [ new PPDPart_Hash( str_repeat( '=', $count ) ) ],
488  'startPos' => $i,
489  'count' => $count ];
490  $stack->push( $piece );
491  $accum =& $stack->getAccum();
492  $stackFlags = $stack->getFlags();
493  if ( isset( $stackFlags['findEquals'] ) ) {
494  $findEquals = $stackFlags['findEquals'];
495  }
496  if ( isset( $stackFlags['findPipe'] ) ) {
497  $findPipe = $stackFlags['findPipe'];
498  }
499  if ( isset( $stackFlags['inHeading'] ) ) {
500  $inHeading = $stackFlags['inHeading'];
501  }
502  $i += $count;
503  }
504  } elseif ( $found == 'line-end' ) {
505  $piece = $stack->top;
506  // A heading must be open, otherwise \n wouldn't have been in the search list
507  // FIXME: Don't use assert()
508  // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.assert
509  assert( $piece->open === "\n" );
510  $part = $piece->getCurrentPart();
511  // Search back through the input to see if it has a proper close.
512  // Do this using the reversed string since the other solutions
513  // (end anchor, etc.) are inefficient.
514  $wsLength = strspn( $revText, " \t", $lengthText - $i );
515  $searchStart = $i - $wsLength;
516  if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
517  // Comment found at line end
518  // Search for equals signs before the comment
519  $searchStart = $part->visualEnd;
520  $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
521  }
522  $count = $piece->count;
523  $equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
524  if ( $equalsLength > 0 ) {
525  if ( $searchStart - $equalsLength == $piece->startPos ) {
526  // This is just a single string of equals signs on its own line
527  // Replicate the doHeadings behavior /={count}(.+)={count}/
528  // First find out how many equals signs there really are (don't stop at 6)
529  $count = $equalsLength;
530  if ( $count < 3 ) {
531  $count = 0;
532  } else {
533  $count = min( 6, intval( ( $count - 1 ) / 2 ) );
534  }
535  } else {
536  $count = min( $equalsLength, $count );
537  }
538  if ( $count > 0 ) {
539  // Normal match, output <h>
540  $element = [ [ 'possible-h',
541  array_merge(
542  [
543  [ '@level', [ $count ] ],
544  [ '@i', [ $headingIndex++ ] ]
545  ],
546  $accum
547  )
548  ] ];
549  } else {
550  // Single equals sign on its own line, count=0
551  $element = $accum;
552  }
553  } else {
554  // No match, no <h>, just pass down the inner text
555  $element = $accum;
556  }
557  // Unwind the stack
558  $stack->pop();
559  $accum =& $stack->getAccum();
560  $stackFlags = $stack->getFlags();
561  if ( isset( $stackFlags['findEquals'] ) ) {
562  $findEquals = $stackFlags['findEquals'];
563  }
564  if ( isset( $stackFlags['findPipe'] ) ) {
565  $findPipe = $stackFlags['findPipe'];
566  }
567  if ( isset( $stackFlags['inHeading'] ) ) {
568  $inHeading = $stackFlags['inHeading'];
569  }
570 
571  // Append the result to the enclosing accumulator
572  array_splice( $accum, count( $accum ), 0, $element );
573 
574  // Note that we do NOT increment the input pointer.
575  // This is because the closing linebreak could be the opening linebreak of
576  // another heading. Infinite loops are avoided because the next iteration MUST
577  // hit the heading open case above, which unconditionally increments the
578  // input pointer.
579  } elseif ( $found == 'open' ) {
580  # count opening brace characters
581  $curLen = strlen( $curChar );
582  $count = ( $curLen > 1 ) ?
583  # allow the final character to repeat
584  strspn( $text, $curChar[$curLen - 1], $i + 1 ) + 1 :
585  strspn( $text, $curChar, $i );
586 
587  $savedPrefix = '';
588  $lineStart = ( $i > 0 && $text[$i - 1] == "\n" );
589 
590  if ( $curChar === "-{" && $count > $curLen ) {
591  // -{ => {{ transition because rightmost wins
592  $savedPrefix = '-';
593  $i++;
594  $curChar = '{';
595  $count--;
596  $rule = $this->rules[$curChar];
597  }
598 
599  # we need to add to stack only if opening brace count is enough for one of the rules
600  if ( $count >= $rule['min'] ) {
601  # Add it to the stack
602  $piece = [
603  'open' => $curChar,
604  'close' => $rule['end'],
605  'savedPrefix' => $savedPrefix,
606  'count' => $count,
607  'lineStart' => $lineStart,
608  ];
609 
610  $stack->push( $piece );
611  $accum =& $stack->getAccum();
612  $stackFlags = $stack->getFlags();
613  if ( isset( $stackFlags['findEquals'] ) ) {
614  $findEquals = $stackFlags['findEquals'];
615  }
616  if ( isset( $stackFlags['findPipe'] ) ) {
617  $findPipe = $stackFlags['findPipe'];
618  }
619  if ( isset( $stackFlags['inHeading'] ) ) {
620  $inHeading = $stackFlags['inHeading'];
621  }
622  } else {
623  # Add literal brace(s)
624  self::addLiteral( $accum, $savedPrefix . str_repeat( $curChar, $count ) );
625  }
626  $i += $count;
627  } elseif ( $found == 'close' ) {
628  $piece = $stack->top;
629  # lets check if there are enough characters for closing brace
630  $maxCount = $piece->count;
631  if ( $piece->close === '}-' && $curChar === '}' ) {
632  $maxCount--; # don't try to match closing '-' as a '}'
633  }
634  $curLen = strlen( $curChar );
635  $count = ( $curLen > 1 ) ? $curLen :
636  strspn( $text, $curChar, $i, $maxCount );
637 
638  # check for maximum matching characters (if there are 5 closing
639  # characters, we will probably need only 3 - depending on the rules)
640  $rule = $this->rules[$piece->open];
641  if ( $count > $rule['max'] ) {
642  # The specified maximum exists in the callback array, unless the caller
643  # has made an error
644  $matchingCount = $rule['max'];
645  } else {
646  # Count is less than the maximum
647  # Skip any gaps in the callback array to find the true largest match
648  # Need to use array_key_exists not isset because the callback can be null
649  $matchingCount = $count;
650  while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
651  --$matchingCount;
652  }
653  }
654 
655  if ( $matchingCount <= 0 ) {
656  # No matching element found in callback array
657  # Output a literal closing brace and continue
658  $endText = substr( $text, $i, $count );
659  self::addLiteral( $accum, $endText );
660  $i += $count;
661  continue;
662  }
663  $name = $rule['names'][$matchingCount];
664  if ( $name === null ) {
665  // No element, just literal text
666  $endText = substr( $text, $i, $matchingCount );
667  $element = $piece->breakSyntax( $matchingCount );
668  self::addLiteral( $element, $endText );
669  } else {
670  # Create XML element
671  $parts = $piece->parts;
672  $titleAccum = $parts[0]->out;
673  unset( $parts[0] );
674 
675  $children = [];
676 
677  # The invocation is at the start of the line if lineStart is set in
678  # the stack, and all opening brackets are used up.
679  if ( $maxCount == $matchingCount &&
680  !empty( $piece->lineStart ) &&
681  strlen( $piece->savedPrefix ) == 0 ) {
682  $children[] = [ '@lineStart', [ 1 ] ];
683  }
684  $titleNode = [ 'title', $titleAccum ];
685  $children[] = $titleNode;
686  $argIndex = 1;
687  foreach ( $parts as $part ) {
688  if ( isset( $part->eqpos ) ) {
689  $equalsNode = $part->out[$part->eqpos];
690  $nameNode = [ 'name', array_slice( $part->out, 0, $part->eqpos ) ];
691  $valueNode = [ 'value', array_slice( $part->out, $part->eqpos + 1 ) ];
692  $partNode = [ 'part', [ $nameNode, $equalsNode, $valueNode ] ];
693  $children[] = $partNode;
694  } else {
695  $nameNode = [ 'name', [ [ '@index', [ $argIndex++ ] ] ] ];
696  $valueNode = [ 'value', $part->out ];
697  $partNode = [ 'part', [ $nameNode, $valueNode ] ];
698  $children[] = $partNode;
699  }
700  }
701  $element = [ [ $name, $children ] ];
702  }
703 
704  # Advance input pointer
705  $i += $matchingCount;
706 
707  # Unwind the stack
708  $stack->pop();
709  $accum =& $stack->getAccum();
710 
711  # Re-add the old stack element if it still has unmatched opening characters remaining
712  if ( $matchingCount < $piece->count ) {
713  $piece->parts = [ new PPDPart_Hash ];
714  $piece->count -= $matchingCount;
715  # do we still qualify for any callback with remaining count?
716  $min = $this->rules[$piece->open]['min'];
717  if ( $piece->count >= $min ) {
718  $stack->push( $piece );
719  $accum =& $stack->getAccum();
720  } elseif ( $piece->count == 1 && $piece->open === '{' && $piece->savedPrefix === '-' ) {
721  $piece->savedPrefix = '';
722  $piece->open = '-{';
723  $piece->count = 2;
724  $piece->close = $this->rules[$piece->open]['end'];
725  $stack->push( $piece );
726  $accum =& $stack->getAccum();
727  } else {
728  $s = substr( $piece->open, 0, -1 );
729  $s .= str_repeat(
730  substr( $piece->open, -1 ),
731  $piece->count - strlen( $s )
732  );
733  self::addLiteral( $accum, $piece->savedPrefix . $s );
734  }
735  } elseif ( $piece->savedPrefix !== '' ) {
736  self::addLiteral( $accum, $piece->savedPrefix );
737  }
738 
739  $stackFlags = $stack->getFlags();
740  if ( isset( $stackFlags['findEquals'] ) ) {
741  $findEquals = $stackFlags['findEquals'];
742  }
743  if ( isset( $stackFlags['findPipe'] ) ) {
744  $findPipe = $stackFlags['findPipe'];
745  }
746  if ( isset( $stackFlags['inHeading'] ) ) {
747  $inHeading = $stackFlags['inHeading'];
748  }
749 
750  # Add XML element to the enclosing accumulator
751  array_splice( $accum, count( $accum ), 0, $element );
752  } elseif ( $found == 'pipe' ) {
753  $findEquals = true; // shortcut for getFlags()
754  $stack->addPart();
755  $accum =& $stack->getAccum();
756  ++$i;
757  } elseif ( $found == 'equals' ) {
758  $findEquals = false; // shortcut for getFlags()
759  $accum[] = [ 'equals', [ '=' ] ];
760  $stack->getCurrentPart()->eqpos = count( $accum ) - 1;
761  ++$i;
762  }
763  }
764 
765  # Output any remaining unclosed brackets
766  foreach ( $stack->stack as $piece ) {
767  array_splice( $stack->rootAccum, count( $stack->rootAccum ), 0, $piece->breakSyntax() );
768  }
769 
770  # Enable top-level headings
771  foreach ( $stack->rootAccum as &$node ) {
772  if ( is_array( $node ) && $node[PPNode_Hash_Tree::NAME] === 'possible-h' ) {
773  $node[PPNode_Hash_Tree::NAME] = 'h';
774  }
775  }
776 
777  $rootStore = [ [ 'root', $stack->rootAccum ] ];
778  $rootNode = new PPNode_Hash_Tree( $rootStore, 0 );
779 
780  // Cache
781  $tree = json_encode( $rootStore, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
782  if ( $tree !== false ) {
783  $this->cacheSetTree( $text, $flags, $tree );
784  }
785 
786  return $rootNode;
787  }
788 
789  private static function addLiteral( array &$accum, $text ) {
790  $n = count( $accum );
791  if ( $n && is_string( $accum[$n - 1] ) ) {
792  $accum[$n - 1] .= $text;
793  } else {
794  $accum[] = $text;
795  }
796  }
797 }
798 
803 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
804 class PPDStack_Hash extends PPDStack {
805 
806  public function __construct() {
807  $this->elementClass = PPDStackElement_Hash::class;
808  parent::__construct();
809  $this->rootAccum = [];
810  }
811 }
812 
816 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
817 class PPDStackElement_Hash extends PPDStackElement {
818 
819  public function __construct( $data = [] ) {
820  $this->partClass = PPDPart_Hash::class;
821  parent::__construct( $data );
822  }
823 
830  public function breakSyntax( $openingCount = false ) {
831  if ( $this->open == "\n" ) {
832  $accum = array_merge( [ $this->savedPrefix ], $this->parts[0]->out );
833  } else {
834  if ( $openingCount === false ) {
835  $openingCount = $this->count;
836  }
837  $s = substr( $this->open, 0, -1 );
838  $s .= str_repeat(
839  substr( $this->open, -1 ),
840  $openingCount - strlen( $s )
841  );
842  $accum = [ $this->savedPrefix . $s ];
843  $lastIndex = 0;
844  $first = true;
845  foreach ( $this->parts as $part ) {
846  if ( $first ) {
847  $first = false;
848  } elseif ( is_string( $accum[$lastIndex] ) ) {
849  $accum[$lastIndex] .= '|';
850  } else {
851  $accum[++$lastIndex] = '|';
852  }
853  foreach ( $part->out as $node ) {
854  if ( is_string( $node ) && is_string( $accum[$lastIndex] ) ) {
855  $accum[$lastIndex] .= $node;
856  } else {
857  $accum[++$lastIndex] = $node;
858  }
859  }
860  }
861  }
862  return $accum;
863  }
864 }
865 
869 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
870 class PPDPart_Hash extends PPDPart {
871 
872  public function __construct( $out = '' ) {
873  if ( $out !== '' ) {
874  $accum = [ $out ];
875  } else {
876  $accum = [];
877  }
878  parent::__construct( $accum );
879  }
880 }
881 
886 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
887 class PPFrame_Hash implements PPFrame {
888 
892  public $parser;
893 
897  public $preprocessor;
898 
902  public $title;
903  public $titleCache;
904 
909  public $loopCheckHash;
910 
915  public $depth;
916 
917  private $volatile = false;
918  private $ttl = null;
919 
923  protected $childExpansionCache;
924 
929  public function __construct( $preprocessor ) {
930  $this->preprocessor = $preprocessor;
931  $this->parser = $preprocessor->parser;
932  $this->title = $this->parser->mTitle;
933  $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
934  $this->loopCheckHash = [];
935  $this->depth = 0;
936  $this->childExpansionCache = [];
937  }
938 
949  public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
950  $namedArgs = [];
951  $numberedArgs = [];
952  if ( $title === false ) {
953  $title = $this->title;
954  }
955  if ( $args !== false ) {
956  if ( $args instanceof PPNode_Hash_Array ) {
957  $args = $args->value;
958  } elseif ( !is_array( $args ) ) {
959  throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
960  }
961  foreach ( $args as $arg ) {
962  $bits = $arg->splitArg();
963  if ( $bits['index'] !== '' ) {
964  // Numbered parameter
965  $index = $bits['index'] - $indexOffset;
966  if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
967  $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
968  wfEscapeWikiText( $this->title ),
969  wfEscapeWikiText( $title ),
970  wfEscapeWikiText( $index ) )->text() );
971  $this->parser->addTrackingCategory( 'duplicate-args-category' );
972  }
973  $numberedArgs[$index] = $bits['value'];
974  unset( $namedArgs[$index] );
975  } else {
976  // Named parameter
977  $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
978  if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
979  $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
980  wfEscapeWikiText( $this->title ),
981  wfEscapeWikiText( $title ),
982  wfEscapeWikiText( $name ) )->text() );
983  $this->parser->addTrackingCategory( 'duplicate-args-category' );
984  }
985  $namedArgs[$name] = $bits['value'];
986  unset( $numberedArgs[$name] );
987  }
988  }
989  }
990  return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
991  }
992 
1000  public function cachedExpand( $key, $root, $flags = 0 ) {
1001  // we don't have a parent, so we don't have a cache
1002  return $this->expand( $root, $flags );
1003  }
1004 
1011  public function expand( $root, $flags = 0 ) {
1012  static $expansionDepth = 0;
1013  if ( is_string( $root ) ) {
1014  return $root;
1015  }
1016 
1017  if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
1018  $this->parser->limitationWarn( 'node-count-exceeded',
1019  $this->parser->mPPNodeCount,
1020  $this->parser->mOptions->getMaxPPNodeCount()
1021  );
1022  return '<span class="error">Node-count limit exceeded</span>';
1023  }
1024  if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
1025  $this->parser->limitationWarn( 'expansion-depth-exceeded',
1026  $expansionDepth,
1027  $this->parser->mOptions->getMaxPPExpandDepth()
1028  );
1029  return '<span class="error">Expansion depth limit exceeded</span>';
1030  }
1031  ++$expansionDepth;
1032  if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
1033  $this->parser->mHighestExpansionDepth = $expansionDepth;
1034  }
1035 
1036  $outStack = [ '', '' ];
1037  $iteratorStack = [ false, $root ];
1038  $indexStack = [ 0, 0 ];
1039 
1040  while ( count( $iteratorStack ) > 1 ) {
1041  $level = count( $outStack ) - 1;
1042  $iteratorNode =& $iteratorStack[$level];
1043  $out =& $outStack[$level];
1044  $index =& $indexStack[$level];
1045 
1046  if ( is_array( $iteratorNode ) ) {
1047  if ( $index >= count( $iteratorNode ) ) {
1048  // All done with this iterator
1049  $iteratorStack[$level] = false;
1050  $contextNode = false;
1051  } else {
1052  $contextNode = $iteratorNode[$index];
1053  $index++;
1054  }
1055  } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
1056  if ( $index >= $iteratorNode->getLength() ) {
1057  // All done with this iterator
1058  $iteratorStack[$level] = false;
1059  $contextNode = false;
1060  } else {
1061  $contextNode = $iteratorNode->item( $index );
1062  $index++;
1063  }
1064  } else {
1065  // Copy to $contextNode and then delete from iterator stack,
1066  // because this is not an iterator but we do have to execute it once
1067  $contextNode = $iteratorStack[$level];
1068  $iteratorStack[$level] = false;
1069  }
1070 
1071  $newIterator = false;
1072  $contextName = false;
1073  $contextChildren = false;
1074 
1075  if ( $contextNode === false ) {
1076  // nothing to do
1077  } elseif ( is_string( $contextNode ) ) {
1078  $out .= $contextNode;
1079  } elseif ( $contextNode instanceof PPNode_Hash_Array ) {
1080  $newIterator = $contextNode;
1081  } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
1082  // No output
1083  } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
1084  $out .= $contextNode->value;
1085  } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
1086  $contextName = $contextNode->name;
1087  $contextChildren = $contextNode->getRawChildren();
1088  } elseif ( is_array( $contextNode ) ) {
1089  // Node descriptor array
1090  if ( count( $contextNode ) !== 2 ) {
1091  throw new MWException( __METHOD__ .
1092  ': found an array where a node descriptor should be' );
1093  }
1094  list( $contextName, $contextChildren ) = $contextNode;
1095  } else {
1096  throw new MWException( __METHOD__ . ': Invalid parameter type' );
1097  }
1098 
1099  // Handle node descriptor array or tree object
1100  if ( $contextName === false ) {
1101  // Not a node, already handled above
1102  } elseif ( $contextName[0] === '@' ) {
1103  // Attribute: no output
1104  } elseif ( $contextName === 'template' ) {
1105  # Double-brace expansion
1106  $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
1107  if ( $flags & PPFrame::NO_TEMPLATES ) {
1108  $newIterator = $this->virtualBracketedImplode(
1109  '{{', '|', '}}',
1110  $bits['title'],
1111  $bits['parts']
1112  );
1113  } else {
1114  $ret = $this->parser->braceSubstitution( $bits, $this );
1115  if ( isset( $ret['object'] ) ) {
1116  $newIterator = $ret['object'];
1117  } else {
1118  $out .= $ret['text'];
1119  }
1120  }
1121  } elseif ( $contextName === 'tplarg' ) {
1122  # Triple-brace expansion
1123  $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
1124  if ( $flags & PPFrame::NO_ARGS ) {
1125  $newIterator = $this->virtualBracketedImplode(
1126  '{{{', '|', '}}}',
1127  $bits['title'],
1128  $bits['parts']
1129  );
1130  } else {
1131  $ret = $this->parser->argSubstitution( $bits, $this );
1132  if ( isset( $ret['object'] ) ) {
1133  $newIterator = $ret['object'];
1134  } else {
1135  $out .= $ret['text'];
1136  }
1137  }
1138  } elseif ( $contextName === 'comment' ) {
1139  # HTML-style comment
1140  # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1141  # Not in RECOVER_COMMENTS mode (msgnw) though.
1142  if ( ( $this->parser->ot['html']
1143  || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1144  || ( $flags & PPFrame::STRIP_COMMENTS )
1145  ) && !( $flags & PPFrame::RECOVER_COMMENTS )
1146  ) {
1147  $out .= '';
1148  } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1149  # Add a strip marker in PST mode so that pstPass2() can
1150  # run some old-fashioned regexes on the result.
1151  # Not in RECOVER_COMMENTS mode (extractSections) though.
1152  $out .= $this->parser->insertStripItem( $contextChildren[0] );
1153  } else {
1154  # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1155  $out .= $contextChildren[0];
1156  }
1157  } elseif ( $contextName === 'ignore' ) {
1158  # Output suppression used by <includeonly> etc.
1159  # OT_WIKI will only respect <ignore> in substed templates.
1160  # The other output types respect it unless NO_IGNORE is set.
1161  # extractSections() sets NO_IGNORE and so never respects it.
1162  if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1163  || ( $flags & PPFrame::NO_IGNORE )
1164  ) {
1165  $out .= $contextChildren[0];
1166  } else {
1167  // $out .= '';
1168  }
1169  } elseif ( $contextName === 'ext' ) {
1170  # Extension tag
1171  $bits = PPNode_Hash_Tree::splitRawExt( $contextChildren ) +
1172  [ 'attr' => null, 'inner' => null, 'close' => null ];
1173  if ( $flags & PPFrame::NO_TAGS ) {
1174  $s = '<' . $bits['name']->getFirstChild()->value;
1175  if ( $bits['attr'] ) {
1176  $s .= $bits['attr']->getFirstChild()->value;
1177  }
1178  if ( $bits['inner'] ) {
1179  $s .= '>' . $bits['inner']->getFirstChild()->value;
1180  if ( $bits['close'] ) {
1181  $s .= $bits['close']->getFirstChild()->value;
1182  }
1183  } else {
1184  $s .= '/>';
1185  }
1186  $out .= $s;
1187  } else {
1188  $out .= $this->parser->extensionSubstitution( $bits, $this );
1189  }
1190  } elseif ( $contextName === 'h' ) {
1191  # Heading
1192  if ( $this->parser->ot['html'] ) {
1193  # Expand immediately and insert heading index marker
1194  $s = $this->expand( $contextChildren, $flags );
1195  $bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
1196  $titleText = $this->title->getPrefixedDBkey();
1197  $this->parser->mHeadings[] = [ $titleText, $bits['i'] ];
1198  $serial = count( $this->parser->mHeadings ) - 1;
1199  $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
1200  $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
1201  $this->parser->mStripState->addGeneral( $marker, '' );
1202  $out .= $s;
1203  } else {
1204  # Expand in virtual stack
1205  $newIterator = $contextChildren;
1206  }
1207  } else {
1208  # Generic recursive expansion
1209  $newIterator = $contextChildren;
1210  }
1211 
1212  if ( $newIterator !== false ) {
1213  $outStack[] = '';
1214  $iteratorStack[] = $newIterator;
1215  $indexStack[] = 0;
1216  } elseif ( $iteratorStack[$level] === false ) {
1217  // Return accumulated value to parent
1218  // With tail recursion
1219  while ( $iteratorStack[$level] === false && $level > 0 ) {
1220  $outStack[$level - 1] .= $out;
1221  array_pop( $outStack );
1222  array_pop( $iteratorStack );
1223  array_pop( $indexStack );
1224  $level--;
1225  }
1226  }
1227  }
1228  --$expansionDepth;
1229  return $outStack[0];
1230  }
1231 
1238  public function implodeWithFlags( $sep, $flags, ...$args ) {
1239  $first = true;
1240  $s = '';
1241  foreach ( $args as $root ) {
1242  if ( $root instanceof PPNode_Hash_Array ) {
1243  $root = $root->value;
1244  }
1245  if ( !is_array( $root ) ) {
1246  $root = [ $root ];
1247  }
1248  foreach ( $root as $node ) {
1249  if ( $first ) {
1250  $first = false;
1251  } else {
1252  $s .= $sep;
1253  }
1254  $s .= $this->expand( $node, $flags );
1255  }
1256  }
1257  return $s;
1258  }
1259 
1267  public function implode( $sep, ...$args ) {
1268  $first = true;
1269  $s = '';
1270  foreach ( $args as $root ) {
1271  if ( $root instanceof PPNode_Hash_Array ) {
1272  $root = $root->value;
1273  }
1274  if ( !is_array( $root ) ) {
1275  $root = [ $root ];
1276  }
1277  foreach ( $root as $node ) {
1278  if ( $first ) {
1279  $first = false;
1280  } else {
1281  $s .= $sep;
1282  }
1283  $s .= $this->expand( $node );
1284  }
1285  }
1286  return $s;
1287  }
1288 
1297  public function virtualImplode( $sep, ...$args ) {
1298  $out = [];
1299  $first = true;
1300 
1301  foreach ( $args as $root ) {
1302  if ( $root instanceof PPNode_Hash_Array ) {
1303  $root = $root->value;
1304  }
1305  if ( !is_array( $root ) ) {
1306  $root = [ $root ];
1307  }
1308  foreach ( $root as $node ) {
1309  if ( $first ) {
1310  $first = false;
1311  } else {
1312  $out[] = $sep;
1313  }
1314  $out[] = $node;
1315  }
1316  }
1317  return new PPNode_Hash_Array( $out );
1318  }
1319 
1329  public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
1330  $out = [ $start ];
1331  $first = true;
1332 
1333  foreach ( $args as $root ) {
1334  if ( $root instanceof PPNode_Hash_Array ) {
1335  $root = $root->value;
1336  }
1337  if ( !is_array( $root ) ) {
1338  $root = [ $root ];
1339  }
1340  foreach ( $root as $node ) {
1341  if ( $first ) {
1342  $first = false;
1343  } else {
1344  $out[] = $sep;
1345  }
1346  $out[] = $node;
1347  }
1348  }
1349  $out[] = $end;
1350  return new PPNode_Hash_Array( $out );
1351  }
1352 
1353  public function __toString() {
1354  return 'frame{}';
1355  }
1356 
1361  public function getPDBK( $level = false ) {
1362  if ( $level === false ) {
1363  return $this->title->getPrefixedDBkey();
1364  } else {
1365  return $this->titleCache[$level] ?? false;
1366  }
1367  }
1368 
1372  public function getArguments() {
1373  return [];
1374  }
1375 
1379  public function getNumberedArguments() {
1380  return [];
1381  }
1382 
1386  public function getNamedArguments() {
1387  return [];
1388  }
1389 
1395  public function isEmpty() {
1396  return true;
1397  }
1398 
1403  public function getArgument( $name ) {
1404  return false;
1405  }
1406 
1414  public function loopCheck( $title ) {
1415  return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1416  }
1417 
1423  public function isTemplate() {
1424  return false;
1425  }
1426 
1432  public function getTitle() {
1433  return $this->title;
1434  }
1435 
1441  public function setVolatile( $flag = true ) {
1442  $this->volatile = $flag;
1443  }
1444 
1450  public function isVolatile() {
1451  return $this->volatile;
1452  }
1453 
1459  public function setTTL( $ttl ) {
1460  if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1461  $this->ttl = $ttl;
1462  }
1463  }
1464 
1470  public function getTTL() {
1471  return $this->ttl;
1472  }
1473 }
1474 
1479 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1480 class PPTemplateFrame_Hash extends PPFrame_Hash {
1481 
1482  public $numberedArgs, $namedArgs, $parent;
1483  public $numberedExpansionCache, $namedExpansionCache;
1484 
1492  public function __construct( $preprocessor, $parent = false, $numberedArgs = [],
1493  $namedArgs = [], $title = false
1494  ) {
1495  parent::__construct( $preprocessor );
1496 
1497  $this->parent = $parent;
1498  $this->numberedArgs = $numberedArgs;
1499  $this->namedArgs = $namedArgs;
1500  $this->title = $title;
1501  $pdbk = $title ? $title->getPrefixedDBkey() : false;
1502  $this->titleCache = $parent->titleCache;
1503  $this->titleCache[] = $pdbk;
1504  $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1505  if ( $pdbk !== false ) {
1506  $this->loopCheckHash[$pdbk] = true;
1507  }
1508  $this->depth = $parent->depth + 1;
1509  $this->numberedExpansionCache = $this->namedExpansionCache = [];
1510  }
1511 
1512  public function __toString() {
1513  $s = 'tplframe{';
1514  $first = true;
1515  $args = $this->numberedArgs + $this->namedArgs;
1516  foreach ( $args as $name => $value ) {
1517  if ( $first ) {
1518  $first = false;
1519  } else {
1520  $s .= ', ';
1521  }
1522  $s .= "\"$name\":\"" .
1523  str_replace( '"', '\\"', $value->__toString() ) . '"';
1524  }
1525  $s .= '}';
1526  return $s;
1527  }
1528 
1536  public function cachedExpand( $key, $root, $flags = 0 ) {
1537  if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1538  return $this->parent->childExpansionCache[$key];
1539  }
1540  $retval = $this->expand( $root, $flags );
1541  if ( !$this->isVolatile() ) {
1542  $this->parent->childExpansionCache[$key] = $retval;
1543  }
1544  return $retval;
1545  }
1546 
1552  public function isEmpty() {
1553  return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1554  }
1555 
1559  public function getArguments() {
1560  $arguments = [];
1561  foreach ( array_merge(
1562  array_keys( $this->numberedArgs ),
1563  array_keys( $this->namedArgs ) ) as $key ) {
1564  $arguments[$key] = $this->getArgument( $key );
1565  }
1566  return $arguments;
1567  }
1568 
1572  public function getNumberedArguments() {
1573  $arguments = [];
1574  foreach ( array_keys( $this->numberedArgs ) as $key ) {
1575  $arguments[$key] = $this->getArgument( $key );
1576  }
1577  return $arguments;
1578  }
1579 
1583  public function getNamedArguments() {
1584  $arguments = [];
1585  foreach ( array_keys( $this->namedArgs ) as $key ) {
1586  $arguments[$key] = $this->getArgument( $key );
1587  }
1588  return $arguments;
1589  }
1590 
1595  public function getNumberedArgument( $index ) {
1596  if ( !isset( $this->numberedArgs[$index] ) ) {
1597  return false;
1598  }
1599  if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1600  # No trimming for unnamed arguments
1601  $this->numberedExpansionCache[$index] = $this->parent->expand(
1602  $this->numberedArgs[$index],
1603  PPFrame::STRIP_COMMENTS
1604  );
1605  }
1606  return $this->numberedExpansionCache[$index];
1607  }
1608 
1613  public function getNamedArgument( $name ) {
1614  if ( !isset( $this->namedArgs[$name] ) ) {
1615  return false;
1616  }
1617  if ( !isset( $this->namedExpansionCache[$name] ) ) {
1618  # Trim named arguments post-expand, for backwards compatibility
1619  $this->namedExpansionCache[$name] = trim(
1620  $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1621  }
1622  return $this->namedExpansionCache[$name];
1623  }
1624 
1629  public function getArgument( $name ) {
1630  $text = $this->getNumberedArgument( $name );
1631  if ( $text === false ) {
1632  $text = $this->getNamedArgument( $name );
1633  }
1634  return $text;
1635  }
1636 
1642  public function isTemplate() {
1643  return true;
1644  }
1645 
1646  public function setVolatile( $flag = true ) {
1647  parent::setVolatile( $flag );
1648  $this->parent->setVolatile( $flag );
1649  }
1650 
1651  public function setTTL( $ttl ) {
1652  parent::setTTL( $ttl );
1653  $this->parent->setTTL( $ttl );
1654  }
1655 }
1656 
1661 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1662 class PPCustomFrame_Hash extends PPFrame_Hash {
1663 
1664  public $args;
1665 
1666  public function __construct( $preprocessor, $args ) {
1667  parent::__construct( $preprocessor );
1668  $this->args = $args;
1669  }
1670 
1671  public function __toString() {
1672  $s = 'cstmframe{';
1673  $first = true;
1674  foreach ( $this->args as $name => $value ) {
1675  if ( $first ) {
1676  $first = false;
1677  } else {
1678  $s .= ', ';
1679  }
1680  $s .= "\"$name\":\"" .
1681  str_replace( '"', '\\"', $value->__toString() ) . '"';
1682  }
1683  $s .= '}';
1684  return $s;
1685  }
1686 
1690  public function isEmpty() {
1691  return !count( $this->args );
1692  }
1693 
1698  public function getArgument( $index ) {
1699  return $this->args[$index] ?? false;
1700  }
1701 
1702  public function getArguments() {
1703  return $this->args;
1704  }
1705 }
1706 
1710 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
1711 class PPNode_Hash_Tree implements PPNode {
1712 
1713  public $name;
1714 
1720  private $rawChildren;
1721 
1725  private $store;
1726 
1730  private $index;
1731 
1736  const NAME = 0;
1737 
1742  const CHILDREN = 1;
1743 
1751  public function __construct( array $store, $index ) {
1752  $this->store = $store;
1753  $this->index = $index;
1754  list( $this->name, $this->rawChildren ) = $this->store[$index];
1755  }
1756 
1766  public static function factory( array $store, $index ) {
1767  if ( !isset( $store[$index] ) ) {
1768  return false;
1769  }
1770 
1771  $descriptor = $store[$index];
1772  if ( is_string( $descriptor ) ) {
1773  $class = PPNode_Hash_Text::class;
1774  } elseif ( is_array( $descriptor ) ) {
1775  if ( $descriptor[self::NAME][0] === '@' ) {
1776  $class = PPNode_Hash_Attr::class;
1777  } else {
1778  $class = self::class;
1779  }
1780  } else {
1781  throw new MWException( __METHOD__ . ': invalid node descriptor' );
1782  }
1783  return new $class( $store, $index );
1784  }
1785 
1790  public function __toString() {
1791  $inner = '';
1792  $attribs = '';
1793  for ( $node = $this->getFirstChild(); $node; $node = $node->getNextSibling() ) {
1794  if ( $node instanceof PPNode_Hash_Attr ) {
1795  $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
1796  } else {
1797  $inner .= $node->__toString();
1798  }
1799  }
1800  if ( $inner === '' ) {
1801  return "<{$this->name}$attribs/>";
1802  } else {
1803  return "<{$this->name}$attribs>$inner</{$this->name}>";
1804  }
1805  }
1806 
1810  public function getChildren() {
1811  $children = [];
1812  foreach ( $this->rawChildren as $i => $child ) {
1813  $children[] = self::factory( $this->rawChildren, $i );
1814  }
1815  return new PPNode_Hash_Array( $children );
1816  }
1817 
1825  public function getFirstChild() {
1826  if ( !isset( $this->rawChildren[0] ) ) {
1827  return false;
1828  } else {
1829  return self::factory( $this->rawChildren, 0 );
1830  }
1831  }
1832 
1840  public function getNextSibling() {
1841  return self::factory( $this->store, $this->index + 1 );
1842  }
1843 
1850  public function getChildrenOfType( $name ) {
1851  $children = [];
1852  foreach ( $this->rawChildren as $i => $child ) {
1853  if ( is_array( $child ) && $child[self::NAME] === $name ) {
1854  $children[] = self::factory( $this->rawChildren, $i );
1855  }
1856  }
1857  return new PPNode_Hash_Array( $children );
1858  }
1859 
1864  public function getRawChildren() {
1865  return $this->rawChildren;
1866  }
1867 
1871  public function getLength() {
1872  return false;
1873  }
1874 
1879  public function item( $i ) {
1880  return false;
1881  }
1882 
1886  public function getName() {
1887  return $this->name;
1888  }
1889 
1899  public function splitArg() {
1900  return self::splitRawArg( $this->rawChildren );
1901  }
1902 
1908  public static function splitRawArg( array $children ) {
1909  $bits = [];
1910  foreach ( $children as $i => $child ) {
1911  if ( !is_array( $child ) ) {
1912  continue;
1913  }
1914  if ( $child[self::NAME] === 'name' ) {
1915  $bits['name'] = new self( $children, $i );
1916  if ( isset( $child[self::CHILDREN][0][self::NAME] )
1917  && $child[self::CHILDREN][0][self::NAME] === '@index'
1918  ) {
1919  $bits['index'] = $child[self::CHILDREN][0][self::CHILDREN][0];
1920  }
1921  } elseif ( $child[self::NAME] === 'value' ) {
1922  $bits['value'] = new self( $children, $i );
1923  }
1924  }
1925 
1926  if ( !isset( $bits['name'] ) ) {
1927  throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
1928  }
1929  if ( !isset( $bits['index'] ) ) {
1930  $bits['index'] = '';
1931  }
1932  return $bits;
1933  }
1934 
1942  public function splitExt() {
1943  return self::splitRawExt( $this->rawChildren );
1944  }
1945 
1951  public static function splitRawExt( array $children ) {
1952  $bits = [];
1953  foreach ( $children as $i => $child ) {
1954  if ( !is_array( $child ) ) {
1955  continue;
1956  }
1957  switch ( $child[self::NAME] ) {
1958  case 'name':
1959  $bits['name'] = new self( $children, $i );
1960  break;
1961  case 'attr':
1962  $bits['attr'] = new self( $children, $i );
1963  break;
1964  case 'inner':
1965  $bits['inner'] = new self( $children, $i );
1966  break;
1967  case 'close':
1968  $bits['close'] = new self( $children, $i );
1969  break;
1970  }
1971  }
1972  if ( !isset( $bits['name'] ) ) {
1973  throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
1974  }
1975  return $bits;
1976  }
1977 
1984  public function splitHeading() {
1985  if ( $this->name !== 'h' ) {
1986  throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1987  }
1988  return self::splitRawHeading( $this->rawChildren );
1989  }
1990 
1996  public static function splitRawHeading( array $children ) {
1997  $bits = [];
1998  foreach ( $children as $i => $child ) {
1999  if ( !is_array( $child ) ) {
2000  continue;
2001  }
2002  if ( $child[self::NAME] === '@i' ) {
2003  $bits['i'] = $child[self::CHILDREN][0];
2004  } elseif ( $child[self::NAME] === '@level' ) {
2005  $bits['level'] = $child[self::CHILDREN][0];
2006  }
2007  }
2008  if ( !isset( $bits['i'] ) ) {
2009  throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
2010  }
2011  return $bits;
2012  }
2013 
2020  public function splitTemplate() {
2021  return self::splitRawTemplate( $this->rawChildren );
2022  }
2023 
2029  public static function splitRawTemplate( array $children ) {
2030  $parts = [];
2031  $bits = [ 'lineStart' => '' ];
2032  foreach ( $children as $i => $child ) {
2033  if ( !is_array( $child ) ) {
2034  continue;
2035  }
2036  switch ( $child[self::NAME] ) {
2037  case 'title':
2038  $bits['title'] = new self( $children, $i );
2039  break;
2040  case 'part':
2041  $parts[] = new self( $children, $i );
2042  break;
2043  case '@lineStart':
2044  $bits['lineStart'] = '1';
2045  break;
2046  }
2047  }
2048  if ( !isset( $bits['title'] ) ) {
2049  throw new MWException( 'Invalid node passed to ' . __METHOD__ );
2050  }
2051  $bits['parts'] = new PPNode_Hash_Array( $parts );
2052  return $bits;
2053  }
2054 }
2055 
2059 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
2060 class PPNode_Hash_Text implements PPNode {
2061 
2062  public $value;
2063  private $store, $index;
2064 
2072  public function __construct( array $store, $index ) {
2073  $this->value = $store[$index];
2074  if ( !is_scalar( $this->value ) ) {
2075  throw new MWException( __CLASS__ . ' given object instead of string' );
2076  }
2077  $this->store = $store;
2078  $this->index = $index;
2079  }
2080 
2081  public function __toString() {
2082  return htmlspecialchars( $this->value );
2083  }
2084 
2085  public function getNextSibling() {
2086  return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
2087  }
2088 
2089  public function getChildren() {
2090  return false;
2091  }
2092 
2093  public function getFirstChild() {
2094  return false;
2095  }
2096 
2097  public function getChildrenOfType( $name ) {
2098  return false;
2099  }
2100 
2101  public function getLength() {
2102  return false;
2103  }
2104 
2105  public function item( $i ) {
2106  return false;
2107  }
2108 
2109  public function getName() {
2110  return '#text';
2111  }
2112 
2113  public function splitArg() {
2114  throw new MWException( __METHOD__ . ': not supported' );
2115  }
2116 
2117  public function splitExt() {
2118  throw new MWException( __METHOD__ . ': not supported' );
2119  }
2120 
2121  public function splitHeading() {
2122  throw new MWException( __METHOD__ . ': not supported' );
2123  }
2124 }
2125 
2129 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
2130 class PPNode_Hash_Array implements PPNode {
2131 
2132  public $value;
2133 
2134  public function __construct( $value ) {
2135  $this->value = $value;
2136  }
2137 
2138  public function __toString() {
2139  return var_export( $this, true );
2140  }
2141 
2142  public function getLength() {
2143  return count( $this->value );
2144  }
2145 
2146  public function item( $i ) {
2147  return $this->value[$i];
2148  }
2149 
2150  public function getName() {
2151  return '#nodelist';
2152  }
2153 
2154  public function getNextSibling() {
2155  return false;
2156  }
2157 
2158  public function getChildren() {
2159  return false;
2160  }
2161 
2162  public function getFirstChild() {
2163  return false;
2164  }
2165 
2166  public function getChildrenOfType( $name ) {
2167  return false;
2168  }
2169 
2170  public function splitArg() {
2171  throw new MWException( __METHOD__ . ': not supported' );
2172  }
2173 
2174  public function splitExt() {
2175  throw new MWException( __METHOD__ . ': not supported' );
2176  }
2177 
2178  public function splitHeading() {
2179  throw new MWException( __METHOD__ . ': not supported' );
2180  }
2181 }
2182 
2186 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
2187 class PPNode_Hash_Attr implements PPNode {
2188 
2189  public $name, $value;
2190  private $store, $index;
2191 
2199  public function __construct( array $store, $index ) {
2200  $descriptor = $store[$index];
2201  if ( $descriptor[PPNode_Hash_Tree::NAME][0] !== '@' ) {
2202  throw new MWException( __METHOD__ . ': invalid name in attribute descriptor' );
2203  }
2204  $this->name = substr( $descriptor[PPNode_Hash_Tree::NAME], 1 );
2205  $this->value = $descriptor[PPNode_Hash_Tree::CHILDREN][0];
2206  $this->store = $store;
2207  $this->index = $index;
2208  }
2209 
2210  public function __toString() {
2211  return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
2212  }
2213 
2214  public function getName() {
2215  return $this->name;
2216  }
2217 
2218  public function getNextSibling() {
2219  return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
2220  }
2221 
2222  public function getChildren() {
2223  return false;
2224  }
2225 
2226  public function getFirstChild() {
2227  return false;
2228  }
2229 
2230  public function getChildrenOfType( $name ) {
2231  return false;
2232  }
2233 
2234  public function getLength() {
2235  return false;
2236  }
2237 
2238  public function item( $i ) {
2239  return false;
2240  }
2241 
2242  public function splitArg() {
2243  throw new MWException( __METHOD__ . ': not supported' );
2244  }
2245 
2246  public function splitExt() {
2247  throw new MWException( __METHOD__ . ': not supported' );
2248  }
2249 
2250  public function splitHeading() {
2251  throw new MWException( __METHOD__ . ': not supported' );
2252  }
2253 }
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
splitArg()
Split a "<part>" node into an associative array containing:
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
cacheGetTree( $text, $flags)
Attempt to load a precomputed document tree for some given wikitext from the cache.
getChildrenOfType( $name)
Get an array of the children with a given node name.
splitTemplate()
Split a "<template>" or "<tplarg>" node.
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
getArguments()
Returns all arguments of this frame.
$index
The index into $this->store which contains the descriptor of this node.
getNextSibling()
Get the next sibling of any node.
getChildrenOfType( $name)
Get all children of this tree node which have a given name.
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
__construct(array $store, $index)
Construct an object using the data from $store[$index].
getFirstChild()
Get the first child, or false if there is none.
in this case you re responsible for computing and outputting the entire conflict part
Definition: hooks.txt:1420
preprocessToObj( $text, $flags=0)
Preprocess some wikitext and return the document tree.
splitExt()
Split an "<ext>" node into an associative array containing name, attr, inner and close All values in ...
__construct(array $store, $index)
Construct an object using the data from $store[$index].
There are three types of nodes:
$value
getLength()
Returns the length of the array, or false if this is not an array-type node.
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
item( $i)
Returns an item of an array-type node.
static splitRawHeading(array $children)
Like splitHeading() but for a raw child array.
static factory(array $store, $index)
Construct an appropriate PPNode_Hash_* object with a class that depends on what is at the relevant st...
getChildren()
Get an array-type node containing the children of this node.
Differences from DOM schema:
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
Stack class to help Preprocessor::preprocessToObj()
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
splitHeading()
Split an "<h>" node.
item( $i)
Returns an item of an array-type node.
getNextSibling()
Get the next sibling, or false if there is none.
getNextSibling()
Get the next sibling of any node.
const NAME
The offset of the name within descriptors, used in some places for readability.
An expansion frame, used as a context to expand the result of preprocessToObj()
getChildrenOfType( $name)
Get all children of this tree node which have a given name.
getChildren()
Get an array-type node containing the children of this node.
splitArg()
Split a "<part>" node into an associative array containing: name PPNode name index String index value...
getChildren()
Get an array-type node containing the children of this node.
getName()
Get the name of this node.
getName()
Get the name of this node.
static splitRawTemplate(array $children)
Like splitTemplate() but for a raw child array.
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 & $attribs
Definition: hooks.txt:1982
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
static splitRawExt(array $children)
Like splitExt() but for a raw child array.
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
item( $i)
Returns an item of an array-type node.
getRawChildren()
Get the raw child array.
getFirstChild()
Get the first child of a tree node.
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.
splitExt()
Split an "<ext>" node into an associative array containing name, attr, inner and close All values in ...
static splitRawArg(array $children)
Like splitArg() but for a raw child array.
getLength()
Returns the length of the array, or false if this is not an array-type node.
splitArg()
Split a "<part>" node into an associative array containing: name PPNode name index String index value...
$store
The store array for the siblings of this node, including this node itself.
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
splitHeading()
Split an "<h>" node.
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
getNextSibling()
Get the next sibling of any node.
$rawChildren
The store array for children of this node.
splitHeading()
Split an "<h>" node.
getLength()
Returns the length of the array, or false if this is not an array-type node.
splitHeading()
Split an "<h>" node.
getChildrenOfType( $name)
Get all children of this tree node which have a given name.
splitExt()
Split an "<ext>" node into an associative array containing name, attr, inner and close All values in ...
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
__toString()
Convert a node to XML, for debugging.
splitExt()
Split an "<ext>" node into an associative array containing name, attr, inner and close All values in ...
Expansion frame with custom arguments.
splitArg()
Split a "<part>" node into an associative array containing: name PPNode name index String index value...
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
getFirstChild()
Get the first child of a tree node.
getFirstChild()
Get the first child of a tree node.
const CHILDREN
The offset of the child list within descriptors, used in some places for readability.
getName()
Get the name of this node.
__construct(array $store, $index)
Construct an object using the data from $store[$index].
$matches