MediaWiki  master
PPFrame_DOM.php
Go to the documentation of this file.
1 <?php
28 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
29 class PPFrame_DOM implements PPFrame {
30 
34  public $preprocessor;
35 
39  public $parser;
40 
44  public $title;
45  public $titleCache;
46 
52 
57  public $depth;
58 
59  private $volatile = false;
60  private $ttl = null;
61 
66 
70  public function __construct( $preprocessor ) {
71  $this->preprocessor = $preprocessor;
72  $this->parser = $preprocessor->parser;
73  $this->title = $this->parser->getTitle();
74  $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
75  $this->loopCheckHash = [];
76  $this->depth = 0;
77  $this->childExpansionCache = [];
78  }
79 
89  public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
90  $namedArgs = [];
91  $numberedArgs = [];
92  if ( $title === false ) {
94  }
95  if ( $args !== false ) {
96  $xpath = false;
97  if ( $args instanceof PPNode_DOM ) {
98  $args = $args->node;
99  }
100  // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
101  foreach ( $args as $arg ) {
102  if ( $arg instanceof PPNode_DOM ) {
103  $arg = $arg->node;
104  }
105  if ( !$xpath || $xpath->document !== $arg->ownerDocument ) {
106  $xpath = new DOMXPath( $arg->ownerDocument );
107  }
108 
109  $nameNodes = $xpath->query( 'name', $arg );
110  $value = $xpath->query( 'value', $arg );
111  if ( $nameNodes->item( 0 )->hasAttributes() ) {
112  // Numbered parameter
113  $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
114  $index = $index - $indexOffset;
115  if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
116  $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
117  wfEscapeWikiText( $this->title ),
119  wfEscapeWikiText( $index ) )->text() );
120  $this->parser->addTrackingCategory( 'duplicate-args-category' );
121  }
122  $numberedArgs[$index] = $value->item( 0 );
123  unset( $namedArgs[$index] );
124  } else {
125  // Named parameter
126  $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) );
127  if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
128  $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
129  wfEscapeWikiText( $this->title ),
131  wfEscapeWikiText( $name ) )->text() );
132  $this->parser->addTrackingCategory( 'duplicate-args-category' );
133  }
134  $namedArgs[$name] = $value->item( 0 );
135  unset( $numberedArgs[$name] );
136  }
137  }
138  }
139  return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
140  }
141 
149  public function cachedExpand( $key, $root, $flags = 0 ) {
150  // we don't have a parent, so we don't have a cache
151  return $this->expand( $root, $flags );
152  }
153 
160  public function expand( $root, $flags = 0 ) {
161  static $expansionDepth = 0;
162  if ( is_string( $root ) ) {
163  return $root;
164  }
165 
166  if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
167  $this->parser->limitationWarn( 'node-count-exceeded',
168  $this->parser->mPPNodeCount,
169  $this->parser->mOptions->getMaxPPNodeCount()
170  );
171  return '<span class="error">Node-count limit exceeded</span>';
172  }
173 
174  if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
175  $this->parser->limitationWarn( 'expansion-depth-exceeded',
176  $expansionDepth,
177  $this->parser->mOptions->getMaxPPExpandDepth()
178  );
179  return '<span class="error">Expansion depth limit exceeded</span>';
180  }
181  ++$expansionDepth;
182  if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
183  $this->parser->mHighestExpansionDepth = $expansionDepth;
184  }
185 
186  if ( $root instanceof PPNode_DOM ) {
187  $root = $root->node;
188  }
189  if ( $root instanceof DOMDocument ) {
190  $root = $root->documentElement;
191  }
192 
193  $outStack = [ '', '' ];
194  $iteratorStack = [ false, $root ];
195  $indexStack = [ 0, 0 ];
196 
197  while ( count( $iteratorStack ) > 1 ) {
198  $level = count( $outStack ) - 1;
199  $iteratorNode =& $iteratorStack[$level];
200  $out =& $outStack[$level];
201  $index =& $indexStack[$level];
202 
203  if ( $iteratorNode instanceof PPNode_DOM ) {
204  $iteratorNode = $iteratorNode->node;
205  }
206 
207  if ( is_array( $iteratorNode ) ) {
208  if ( $index >= count( $iteratorNode ) ) {
209  // All done with this iterator
210  $iteratorStack[$level] = false;
211  $contextNode = false;
212  } else {
213  $contextNode = $iteratorNode[$index];
214  $index++;
215  }
216  } elseif ( $iteratorNode instanceof DOMNodeList ) {
217  if ( $index >= $iteratorNode->length ) {
218  // All done with this iterator
219  $iteratorStack[$level] = false;
220  $contextNode = false;
221  } else {
222  $contextNode = $iteratorNode->item( $index );
223  $index++;
224  }
225  } else {
226  // Copy to $contextNode and then delete from iterator stack,
227  // because this is not an iterator but we do have to execute it once
228  $contextNode = $iteratorStack[$level];
229  $iteratorStack[$level] = false;
230  }
231 
232  if ( $contextNode instanceof PPNode_DOM ) {
233  $contextNode = $contextNode->node;
234  }
235 
236  $newIterator = false;
237 
238  if ( $contextNode === false ) {
239  // nothing to do
240  } elseif ( is_string( $contextNode ) ) {
241  $out .= $contextNode;
242  } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) {
243  $newIterator = $contextNode;
244  } elseif ( $contextNode instanceof DOMNode ) {
245  if ( $contextNode->nodeType == XML_TEXT_NODE ) {
246  $out .= $contextNode->nodeValue;
247  } elseif ( $contextNode->nodeName == 'template' ) {
248  # Double-brace expansion
249  $xpath = new DOMXPath( $contextNode->ownerDocument );
250  $titles = $xpath->query( 'title', $contextNode );
251  $title = $titles->item( 0 );
252  $parts = $xpath->query( 'part', $contextNode );
253  if ( $flags & PPFrame::NO_TEMPLATES ) {
254  $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
255  } else {
256  $lineStart = $contextNode->getAttribute( 'lineStart' );
257  $params = [
258  'title' => new PPNode_DOM( $title ),
259  'parts' => new PPNode_DOM( $parts ),
260  'lineStart' => $lineStart ];
261  $ret = $this->parser->braceSubstitution( $params, $this );
262  if ( isset( $ret['object'] ) ) {
263  $newIterator = $ret['object'];
264  } else {
265  $out .= $ret['text'];
266  }
267  }
268  } elseif ( $contextNode->nodeName == 'tplarg' ) {
269  # Triple-brace expansion
270  $xpath = new DOMXPath( $contextNode->ownerDocument );
271  $titles = $xpath->query( 'title', $contextNode );
272  $title = $titles->item( 0 );
273  $parts = $xpath->query( 'part', $contextNode );
274  if ( $flags & PPFrame::NO_ARGS ) {
275  $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
276  } else {
277  $params = [
278  'title' => new PPNode_DOM( $title ),
279  'parts' => new PPNode_DOM( $parts ) ];
280  $ret = $this->parser->argSubstitution( $params, $this );
281  if ( isset( $ret['object'] ) ) {
282  $newIterator = $ret['object'];
283  } else {
284  $out .= $ret['text'];
285  }
286  }
287  } elseif ( $contextNode->nodeName == 'comment' ) {
288  # HTML-style comment
289  # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
290  # Not in RECOVER_COMMENTS mode (msgnw) though.
291  if ( ( $this->parser->ot['html']
292  || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
293  || ( $flags & PPFrame::STRIP_COMMENTS )
294  ) && !( $flags & PPFrame::RECOVER_COMMENTS )
295  ) {
296  $out .= '';
297  } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
298  # Add a strip marker in PST mode so that pstPass2() can
299  # run some old-fashioned regexes on the result.
300  # Not in RECOVER_COMMENTS mode (extractSections) though.
301  $out .= $this->parser->insertStripItem( $contextNode->textContent );
302  } else {
303  # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
304  $out .= $contextNode->textContent;
305  }
306  } elseif ( $contextNode->nodeName == 'ignore' ) {
307  # Output suppression used by <includeonly> etc.
308  # OT_WIKI will only respect <ignore> in substed templates.
309  # The other output types respect it unless NO_IGNORE is set.
310  # extractSections() sets NO_IGNORE and so never respects it.
311  if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
312  || ( $flags & PPFrame::NO_IGNORE )
313  ) {
314  $out .= $contextNode->textContent;
315  } else {
316  $out .= '';
317  }
318  } elseif ( $contextNode->nodeName == 'ext' ) {
319  # Extension tag
320  $xpath = new DOMXPath( $contextNode->ownerDocument );
321  $names = $xpath->query( 'name', $contextNode );
322  $attrs = $xpath->query( 'attr', $contextNode );
323  $inners = $xpath->query( 'inner', $contextNode );
324  $closes = $xpath->query( 'close', $contextNode );
325  if ( $flags & PPFrame::NO_TAGS ) {
326  $s = '<' . $this->expand( $names->item( 0 ), $flags );
327  if ( $attrs->length > 0 ) {
328  $s .= $this->expand( $attrs->item( 0 ), $flags );
329  }
330  if ( $inners->length > 0 ) {
331  $s .= '>' . $this->expand( $inners->item( 0 ), $flags );
332  if ( $closes->length > 0 ) {
333  $s .= $this->expand( $closes->item( 0 ), $flags );
334  }
335  } else {
336  $s .= '/>';
337  }
338  $out .= $s;
339  } else {
340  $params = [
341  'name' => new PPNode_DOM( $names->item( 0 ) ),
342  'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null,
343  'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null,
344  'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null,
345  ];
346  $out .= $this->parser->extensionSubstitution( $params, $this );
347  }
348  } elseif ( $contextNode->nodeName == 'h' ) {
349  # Heading
350  $s = $this->expand( $contextNode->childNodes, $flags );
351 
352  # Insert a heading marker only for <h> children of <root>
353  # This is to stop extractSections from going over multiple tree levels
354  if ( $contextNode->parentNode->nodeName == 'root' && $this->parser->ot['html'] ) {
355  # Insert heading index marker
356  $headingIndex = $contextNode->getAttribute( 'i' );
357  $titleText = $this->title->getPrefixedDBkey();
358  $this->parser->mHeadings[] = [ $titleText, $headingIndex ];
359  $serial = count( $this->parser->mHeadings ) - 1;
360  $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
361  $count = $contextNode->getAttribute( 'level' );
362  $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
363  $this->parser->mStripState->addGeneral( $marker, '' );
364  }
365  $out .= $s;
366  } else {
367  # Generic recursive expansion
368  $newIterator = $contextNode->childNodes;
369  }
370  } else {
371  throw new MWException( __METHOD__ . ': Invalid parameter type' );
372  }
373 
374  if ( $newIterator !== false ) {
375  if ( $newIterator instanceof PPNode_DOM ) {
376  $newIterator = $newIterator->node;
377  }
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_DOM ) {
408  $root = $root->node;
409  }
410  if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
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 
433  public function implode( $sep, ...$args ) {
434  $first = true;
435  $s = '';
436  foreach ( $args as $root ) {
437  if ( $root instanceof PPNode_DOM ) {
438  $root = $root->node;
439  }
440  if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
441  $root = [ $root ];
442  }
443  foreach ( $root as $node ) {
444  if ( $first ) {
445  $first = false;
446  } else {
447  $s .= $sep;
448  }
449  $s .= $this->expand( $node );
450  }
451  }
452  return $s;
453  }
454 
464  public function virtualImplode( $sep, ...$args ) {
465  $out = [];
466  $first = true;
467 
468  foreach ( $args as $root ) {
469  if ( $root instanceof PPNode_DOM ) {
470  $root = $root->node;
471  }
472  if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
473  $root = [ $root ];
474  }
475  foreach ( $root as $node ) {
476  if ( $first ) {
477  $first = false;
478  } else {
479  $out[] = $sep;
480  }
481  $out[] = $node;
482  }
483  }
484  return $out;
485  }
486 
496  public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
497  $out = [ $start ];
498  $first = true;
499 
500  foreach ( $args as $root ) {
501  if ( $root instanceof PPNode_DOM ) {
502  $root = $root->node;
503  }
504  if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
505  $root = [ $root ];
506  }
507  foreach ( $root as $node ) {
508  if ( $first ) {
509  $first = false;
510  } else {
511  $out[] = $sep;
512  }
513  $out[] = $node;
514  }
515  }
516  $out[] = $end;
517  return $out;
518  }
519 
520  public function __toString() {
521  return 'frame{}';
522  }
523 
524  public function getPDBK( $level = false ) {
525  if ( $level === false ) {
526  return $this->title->getPrefixedDBkey();
527  } else {
528  return $this->titleCache[$level] ?? false;
529  }
530  }
531 
535  public function getArguments() {
536  return [];
537  }
538 
542  public function getNumberedArguments() {
543  return [];
544  }
545 
549  public function getNamedArguments() {
550  return [];
551  }
552 
558  public function isEmpty() {
559  return true;
560  }
561 
566  public function getArgument( $name ) {
567  return false;
568  }
569 
576  public function loopCheck( $title ) {
577  return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
578  }
579 
585  public function isTemplate() {
586  return false;
587  }
588 
594  public function getTitle() {
595  return $this->title;
596  }
597 
603  public function setVolatile( $flag = true ) {
604  $this->volatile = $flag;
605  }
606 
612  public function isVolatile() {
613  return $this->volatile;
614  }
615 
621  public function setTTL( $ttl ) {
622  if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
623  $this->ttl = $ttl;
624  }
625  }
626 
632  public function getTTL() {
633  return $this->ttl;
634  }
635 }
const MARKER_PREFIX
Definition: Parser.php:139
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
getTTL()
Get the TTL.
getNumberedArguments()
getArgument( $name)
Expansion frame with template arguments.
virtualBracketedImplode( $start, $sep, $end,... $args)
Virtual implode with brackets.
getTitle()
Get a title of frame.
const RECOVER_COMMENTS
Definition: PPFrame.php:33
expand( $root, $flags=0)
const NO_ARGS
Definition: PPFrame.php:29
cachedExpand( $key, $root, $flags=0)
const NO_IGNORE
Definition: PPFrame.php:32
isTemplate()
Return true if the frame is a template frame.
if( $line===false) $args
Definition: cdb.php:64
const NO_TEMPLATES
Definition: PPFrame.php:30
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...
Definition: PPFrame_DOM.php:89
Title $title
Definition: PPFrame_DOM.php:44
implode( $sep,... $args)
Implode with no flags specified This previously called implodeWithFlags but has now been inlined to r...
implodeWithFlags( $sep, $flags,... $args)
setVolatile( $flag=true)
Set the volatile flag.
loopCheck( $title)
Returns true if the infinite loop check is OK, false if a loop is detected.
Preprocessor $preprocessor
Definition: PPFrame_DOM.php:34
setTTL( $ttl)
Set the TTL.
An expansion frame, used as a context to expand the result of preprocessToObj()
Definition: PPFrame_DOM.php:29
getPDBK( $level=false)
$loopCheckHash
Hashtable listing templates which are disallowed for expansion in this frame, having been encountered...
Definition: PPFrame_DOM.php:51
__construct( $preprocessor)
Definition: PPFrame_DOM.php:70
virtualImplode( $sep,... $args)
Makes an object that, when expand()ed, will be the same as one obtained with implode() ...
const NO_TAGS
Definition: PPFrame.php:34
isVolatile()
Get the volatile flag.
Parser $parser
Definition: PPFrame_DOM.php:39
array $childExpansionCache
Definition: PPFrame_DOM.php:65
$depth
Recursion depth of this frame, top = 0 Note that this is NOT the same as expansion depth in expand() ...
Definition: PPFrame_DOM.php:57
isEmpty()
Returns true if there are no arguments in this frame.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
const STRIP_COMMENTS
Definition: PPFrame.php:31
getPrefixedDBkey()
Get the prefixed database key form.
Definition: Title.php:1846