MediaWiki  master
PPFrame_Hash.php
Go to the documentation of this file.
1 <?php
26 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
27 class PPFrame_Hash implements PPFrame {
28 
32  public $parser;
33 
37  public $preprocessor;
38 
42  public $title;
43 
47  public $titleCache;
48 
55 
61  public $depth;
62 
64  private $volatile = false;
66  private $ttl = null;
67 
75  private $maxPPNodeCount;
80 
84  public function __construct( $preprocessor ) {
85  $this->preprocessor = $preprocessor;
86  $this->parser = $preprocessor->parser;
87  $this->title = $this->parser->getTitle();
88  $this->maxPPNodeCount = $this->parser->getOptions()->getMaxPPNodeCount();
89  $this->maxPPExpandDepth = $this->parser->getOptions()->getMaxPPExpandDepth();
90  $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
91  $this->loopCheckHash = [];
92  $this->depth = 0;
93  $this->childExpansionCache = [];
94  }
95 
106  public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
107  $namedArgs = [];
108  $numberedArgs = [];
109  if ( $title === false ) {
111  }
112  if ( $args !== false ) {
113  if ( $args instanceof PPNode_Hash_Array ) {
114  $args = $args->value;
115  } elseif ( !is_array( $args ) ) {
116  throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
117  }
118  foreach ( $args as $arg ) {
119  $bits = $arg->splitArg();
120  if ( $bits['index'] !== '' ) {
121  // Numbered parameter
122  $index = $bits['index'] - $indexOffset;
123  if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
124  $this->parser->getOutput()->addWarningMsg(
125  'duplicate-args-warning',
126  Message::plaintextParam( (string)$this->title ),
127  Message::plaintextParam( (string)$title ),
128  Message::numParam( $index )
129  );
130  $this->parser->addTrackingCategory( 'duplicate-args-category' );
131  }
132  $numberedArgs[$index] = $bits['value'];
133  unset( $namedArgs[$index] );
134  } else {
135  // Named parameter
136  $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
137  if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
138  $this->parser->getOutput()->addWarningMsg(
139  'duplicate-args-warning',
140  Message::plaintextParam( (string)$this->title ),
141  Message::plaintextParam( (string)$title ),
142  Message::plaintextParam( $name )
143  );
144  $this->parser->addTrackingCategory( 'duplicate-args-category' );
145  }
146  $namedArgs[$name] = $bits['value'];
147  unset( $numberedArgs[$name] );
148  }
149  }
150  }
151  // @phan-suppress-next-line SecurityCheck-XSS taint track for keys in named args, false positive
152  return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
153  }
154 
162  public function cachedExpand( $key, $root, $flags = 0 ) {
163  // we don't have a parent, so we don't have a cache
164  return $this->expand( $root, $flags );
165  }
166 
173  public function expand( $root, $flags = 0 ) {
174  static $expansionDepth = 0;
175  if ( is_string( $root ) ) {
176  return $root;
177  }
178 
179  if ( ++$this->parser->mPPNodeCount > $this->maxPPNodeCount ) {
180  $this->parser->limitationWarn( 'node-count-exceeded',
181  $this->parser->mPPNodeCount,
182  $this->maxPPNodeCount
183  );
184  return '<span class="error">Node-count limit exceeded</span>';
185  }
186  if ( $expansionDepth > $this->maxPPExpandDepth ) {
187  $this->parser->limitationWarn( 'expansion-depth-exceeded',
188  $expansionDepth,
189  $this->maxPPExpandDepth
190  );
191  return '<span class="error">Expansion depth limit exceeded</span>';
192  }
193  ++$expansionDepth;
194  if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
195  $this->parser->mHighestExpansionDepth = $expansionDepth;
196  }
197 
198  $outStack = [ '', '' ];
199  $iteratorStack = [ false, $root ];
200  $indexStack = [ 0, 0 ];
201 
202  while ( count( $iteratorStack ) > 1 ) {
203  $level = count( $outStack ) - 1;
204  $iteratorNode =& $iteratorStack[$level];
205  $out =& $outStack[$level];
206  $index =& $indexStack[$level];
207 
208  if ( is_array( $iteratorNode ) ) {
209  if ( $index >= count( $iteratorNode ) ) {
210  // All done with this iterator
211  $iteratorStack[$level] = false;
212  $contextNode = false;
213  } else {
214  $contextNode = $iteratorNode[$index];
215  $index++;
216  }
217  } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
218  if ( $index >= $iteratorNode->getLength() ) {
219  // All done with this iterator
220  $iteratorStack[$level] = false;
221  $contextNode = false;
222  } else {
223  $contextNode = $iteratorNode->item( $index );
224  $index++;
225  }
226  } else {
227  // Copy to $contextNode and then delete from iterator stack,
228  // because this is not an iterator but we do have to execute it once
229  $contextNode = $iteratorStack[$level];
230  $iteratorStack[$level] = false;
231  }
232 
233  $newIterator = false;
234  $contextName = false;
235  $contextChildren = false;
236 
237  if ( $contextNode === false ) {
238  // nothing to do
239  } elseif ( is_string( $contextNode ) ) {
240  $out .= $contextNode;
241  } elseif ( $contextNode instanceof PPNode_Hash_Array ) {
242  $newIterator = $contextNode;
243  } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
244  // No output
245  } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
246  $out .= $contextNode->value;
247  } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
248  $contextName = $contextNode->name;
249  $contextChildren = $contextNode->getRawChildren();
250  } elseif ( is_array( $contextNode ) ) {
251  // Node descriptor array
252  if ( count( $contextNode ) !== 2 ) {
253  throw new MWException( __METHOD__ .
254  ': found an array where a node descriptor should be' );
255  }
256  list( $contextName, $contextChildren ) = $contextNode;
257  } else {
258  throw new MWException( __METHOD__ . ': Invalid parameter type' );
259  }
260 
261  // Handle node descriptor array or tree object
262  if ( $contextName === false ) {
263  // Not a node, already handled above
264  } elseif ( $contextName[0] === '@' ) {
265  // Attribute: no output
266  } elseif ( $contextName === 'template' ) {
267  # Double-brace expansion
268  $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
269  if ( $flags & PPFrame::NO_TEMPLATES ) {
270  $newIterator = $this->virtualBracketedImplode(
271  '{{', '|', '}}',
272  $bits['title'],
273  $bits['parts']
274  );
275  } else {
276  $ret = $this->parser->braceSubstitution( $bits, $this );
277  if ( isset( $ret['object'] ) ) {
278  $newIterator = $ret['object'];
279  } else {
280  $out .= $ret['text'];
281  }
282  }
283  } elseif ( $contextName === 'tplarg' ) {
284  # Triple-brace expansion
285  $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
286  if ( $flags & PPFrame::NO_ARGS ) {
287  $newIterator = $this->virtualBracketedImplode(
288  '{{{', '|', '}}}',
289  $bits['title'],
290  $bits['parts']
291  );
292  } else {
293  $ret = $this->parser->argSubstitution( $bits, $this );
294  if ( isset( $ret['object'] ) ) {
295  $newIterator = $ret['object'];
296  } else {
297  $out .= $ret['text'];
298  }
299  }
300  } elseif ( $contextName === 'comment' ) {
301  # HTML-style comment
302  # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
303  # Not in RECOVER_COMMENTS mode (msgnw) though.
304  if ( ( $this->parser->ot['html']
305  || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
306  || ( $flags & PPFrame::STRIP_COMMENTS )
307  ) && !( $flags & PPFrame::RECOVER_COMMENTS )
308  ) {
309  $out .= '';
310  } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
311  # Add a strip marker in PST mode so that pstPass2() can
312  # run some old-fashioned regexes on the result.
313  # Not in RECOVER_COMMENTS mode (extractSections) though.
314  $out .= $this->parser->insertStripItem( $contextChildren[0] );
315  } else {
316  # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
317  $out .= $contextChildren[0];
318  }
319  } elseif ( $contextName === 'ignore' ) {
320  # Output suppression used by <includeonly> etc.
321  # OT_WIKI will only respect <ignore> in substed templates.
322  # The other output types respect it unless NO_IGNORE is set.
323  # extractSections() sets NO_IGNORE and so never respects it.
324  if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
325  || ( $flags & PPFrame::NO_IGNORE )
326  ) {
327  $out .= $contextChildren[0];
328  } else {
329  // $out .= '';
330  }
331  } elseif ( $contextName === 'ext' ) {
332  # Extension tag
333  $bits = PPNode_Hash_Tree::splitRawExt( $contextChildren ) +
334  [ 'attr' => null, 'inner' => null, 'close' => null ];
335  if ( $flags & PPFrame::NO_TAGS ) {
336  $s = '<' . $bits['name']->getFirstChild()->value;
337  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
338  if ( $bits['attr'] ) {
339  $s .= $bits['attr']->getFirstChild()->value;
340  }
341  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
342  if ( $bits['inner'] ) {
343  $s .= '>' . $bits['inner']->getFirstChild()->value;
344  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
345  if ( $bits['close'] ) {
346  $s .= $bits['close']->getFirstChild()->value;
347  }
348  } else {
349  $s .= '/>';
350  }
351  $out .= $s;
352  } else {
353  $out .= $this->parser->extensionSubstitution( $bits, $this );
354  }
355  } elseif ( $contextName === 'h' ) {
356  # Heading
357  if ( $this->parser->ot['html'] ) {
358  # Expand immediately and insert heading index marker
359  $s = $this->expand( $contextChildren, $flags );
360  $bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
361  $titleText = $this->title->getPrefixedDBkey();
362  $this->parser->mHeadings[] = [ $titleText, $bits['i'] ];
363  $serial = count( $this->parser->mHeadings ) - 1;
364  $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
365  $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
366  $this->parser->getStripState()->addGeneral( $marker, '' );
367  $out .= $s;
368  } else {
369  # Expand in virtual stack
370  $newIterator = $contextChildren;
371  }
372  } else {
373  # Generic recursive expansion
374  $newIterator = $contextChildren;
375  }
376 
377  if ( $newIterator !== false ) {
378  $outStack[] = '';
379  $iteratorStack[] = $newIterator;
380  $indexStack[] = 0;
381  } elseif ( $iteratorStack[$level] === false ) {
382  // Return accumulated value to parent
383  // With tail recursion
384  while ( $iteratorStack[$level] === false && $level > 0 ) {
385  $outStack[$level - 1] .= $out;
386  array_pop( $outStack );
387  array_pop( $iteratorStack );
388  array_pop( $indexStack );
389  $level--;
390  }
391  }
392  }
393  --$expansionDepth;
394  return $outStack[0];
395  }
396 
403  public function implodeWithFlags( $sep, $flags, ...$args ) {
404  $first = true;
405  $s = '';
406  foreach ( $args as $root ) {
407  if ( $root instanceof PPNode_Hash_Array ) {
408  $root = $root->value;
409  }
410  if ( !is_array( $root ) ) {
411  $root = [ $root ];
412  }
413  foreach ( $root as $node ) {
414  if ( $first ) {
415  $first = false;
416  } else {
417  $s .= $sep;
418  }
419  $s .= $this->expand( $node, $flags );
420  }
421  }
422  return $s;
423  }
424 
432  public function implode( $sep, ...$args ) {
433  $first = true;
434  $s = '';
435  foreach ( $args as $root ) {
436  if ( $root instanceof PPNode_Hash_Array ) {
437  $root = $root->value;
438  }
439  if ( !is_array( $root ) ) {
440  $root = [ $root ];
441  }
442  foreach ( $root as $node ) {
443  if ( $first ) {
444  $first = false;
445  } else {
446  $s .= $sep;
447  }
448  $s .= $this->expand( $node );
449  }
450  }
451  return $s;
452  }
453 
462  public function virtualImplode( $sep, ...$args ) {
463  $out = [];
464  $first = true;
465 
466  foreach ( $args as $root ) {
467  if ( $root instanceof PPNode_Hash_Array ) {
468  $root = $root->value;
469  }
470  if ( !is_array( $root ) ) {
471  $root = [ $root ];
472  }
473  foreach ( $root as $node ) {
474  if ( $first ) {
475  $first = false;
476  } else {
477  $out[] = $sep;
478  }
479  $out[] = $node;
480  }
481  }
482  return new PPNode_Hash_Array( $out );
483  }
484 
494  public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
495  $out = [ $start ];
496  $first = true;
497 
498  foreach ( $args as $root ) {
499  if ( $root instanceof PPNode_Hash_Array ) {
500  $root = $root->value;
501  }
502  if ( !is_array( $root ) ) {
503  $root = [ $root ];
504  }
505  foreach ( $root as $node ) {
506  if ( $first ) {
507  $first = false;
508  } else {
509  $out[] = $sep;
510  }
511  $out[] = $node;
512  }
513  }
514  $out[] = $end;
515  return new PPNode_Hash_Array( $out );
516  }
517 
518  public function __toString() {
519  return 'frame{}';
520  }
521 
526  public function getPDBK( $level = false ) {
527  if ( $level === false ) {
528  return $this->title->getPrefixedDBkey();
529  } else {
530  return $this->titleCache[$level] ?? false;
531  }
532  }
533 
537  public function getArguments() {
538  return [];
539  }
540 
544  public function getNumberedArguments() {
545  return [];
546  }
547 
551  public function getNamedArguments() {
552  return [];
553  }
554 
560  public function isEmpty() {
561  return true;
562  }
563 
568  public function getArgument( $name ) {
569  return false;
570  }
571 
579  public function loopCheck( $title ) {
580  return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
581  }
582 
588  public function isTemplate() {
589  return false;
590  }
591 
597  public function getTitle() {
598  return $this->title;
599  }
600 
606  public function setVolatile( $flag = true ) {
607  $this->volatile = $flag;
608  }
609 
615  public function isVolatile() {
616  return $this->volatile;
617  }
618 
622  public function setTTL( $ttl ) {
623  if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
624  $this->ttl = $ttl;
625  }
626  }
627 
631  public function getTTL() {
632  return $this->ttl;
633  }
634 }
MediaWiki exception.
Definition: MWException.php:31
static plaintextParam( $plaintext)
Definition: Message.php:1287
static numParam( $num)
Definition: Message.php:1166
An expansion frame, used as a context to expand the result of preprocessToObj()
getArgument( $name)
isEmpty()
Returns true if there are no arguments in this frame.
int $depth
Recursion depth of this frame, top = 0 Note that this is NOT the same as expansion depth in expand()
loopCheck( $title)
Returns true if the infinite loop check is OK, false if a loop is detected.
int null $ttl
setVolatile( $flag=true)
Set the volatile flag.
string false[] $titleCache
expand( $root, $flags=0)
implodeWithFlags( $sep, $flags,... $args)
getTitle()
Get a title of frame.
Preprocessor $preprocessor
implode( $sep,... $args)
Implode with no flags specified This previously called implodeWithFlags but has now been inlined to r...
true[] $loopCheckHash
Hashtable listing templates which are disallowed for expansion in this frame, having been encountered...
cachedExpand( $key, $root, $flags=0)
__construct( $preprocessor)
array $childExpansionCache
virtualImplode( $sep,... $args)
Makes an object that, when expand()ed, will be the same as one obtained with implode()
virtualBracketedImplode( $start, $sep, $end,... $args)
Virtual implode with brackets.
isVolatile()
Get the volatile flag.
int $maxPPExpandDepth
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...
Parser $parser
isTemplate()
Return true if the frame is a template frame.
getPDBK( $level=false)
static splitRawTemplate(array $children)
Like splitTemplate() but for a raw child array.
static splitRawHeading(array $children)
Like splitHeading() but for a raw child array.
static splitRawExt(array $children)
Like splitExt() but for a raw child array.
Expansion frame with template arguments.
const MARKER_PREFIX
Definition: Parser.php:154
getPrefixedDBkey()
Get the prefixed database key form.
Definition: Title.php:1874
const NO_TEMPLATES
Definition: PPFrame.php:30
const NO_TAGS
Definition: PPFrame.php:34
const RECOVER_COMMENTS
Definition: PPFrame.php:33
const NO_ARGS
Definition: PPFrame.php:29
const NO_IGNORE
Definition: PPFrame.php:32
const STRIP_COMMENTS
Definition: PPFrame.php:31
if( $line===false) $args
Definition: mcc.php:124
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s