Merge "recompressTracked.php: Fix typehint of RecompressTracked::dispatch()"
[lhc/web/wiklou.git] / includes / parser / PPFrame_DOM.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup Parser
20 */
21
22 /**
23 * An expansion frame, used as a context to expand the result of preprocessToObj()
24 * @deprecated since 1.34, use PPFrame_Hash
25 * @ingroup Parser
26 * @phan-file-suppress PhanUndeclaredMethod
27 */
28 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
29 class PPFrame_DOM implements PPFrame {
30
31 /**
32 * @var Preprocessor
33 */
34 public $preprocessor;
35
36 /**
37 * @var Parser
38 */
39 public $parser;
40
41 /**
42 * @var Title
43 */
44 public $title;
45 public $titleCache;
46
47 /**
48 * Hashtable listing templates which are disallowed for expansion in this frame,
49 * having been encountered previously in parent frames.
50 */
51 public $loopCheckHash;
52
53 /**
54 * Recursion depth of this frame, top = 0
55 * Note that this is NOT the same as expansion depth in expand()
56 */
57 public $depth;
58
59 private $volatile = false;
60 private $ttl = null;
61
62 /**
63 * @var array
64 */
65 protected $childExpansionCache;
66
67 /**
68 * Construct a new preprocessor frame.
69 * @param Preprocessor $preprocessor The parent preprocessor
70 */
71 public function __construct( $preprocessor ) {
72 $this->preprocessor = $preprocessor;
73 $this->parser = $preprocessor->parser;
74 $this->title = $this->parser->getTitle();
75 $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
76 $this->loopCheckHash = [];
77 $this->depth = 0;
78 $this->childExpansionCache = [];
79 }
80
81 /**
82 * Create a new child frame
83 * $args is optionally a multi-root PPNode or array containing the template arguments
84 *
85 * @param bool|array $args
86 * @param Title|bool $title
87 * @param int $indexOffset
88 * @return PPTemplateFrame_DOM
89 */
90 public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
91 $namedArgs = [];
92 $numberedArgs = [];
93 if ( $title === false ) {
94 $title = $this->title;
95 }
96 if ( $args !== false ) {
97 $xpath = false;
98 if ( $args instanceof PPNode ) {
99 $args = $args->node;
100 }
101 foreach ( $args as $arg ) {
102 if ( $arg instanceof PPNode ) {
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 ),
118 wfEscapeWikiText( $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 ),
130 wfEscapeWikiText( $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
142 /**
143 * @throws MWException
144 * @param string|int $key
145 * @param string|PPNode_DOM|DOMNode|DOMNodeList $root
146 * @param int $flags
147 * @return string
148 */
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
154 /**
155 * @throws MWException
156 * @param string|PPNode_DOM|DOMNode|DOMNodeList $root
157 * @param int $flags
158 * @return string
159 */
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
397 /**
398 * @param string $sep
399 * @param int $flags
400 * @param string|PPNode_DOM|DOMNode ...$args
401 * @return string
402 */
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
425 /**
426 * Implode with no flags specified
427 * This previously called implodeWithFlags but has now been inlined to reduce stack depth
428 *
429 * @param string $sep
430 * @param string|PPNode_DOM|DOMNode ...$args
431 * @return string
432 */
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
455 /**
456 * Makes an object that, when expand()ed, will be the same as one obtained
457 * with implode()
458 *
459 * @param string $sep
460 * @param string|PPNode_DOM|DOMNode ...$args
461 * @return array
462 * @suppress PhanParamSignatureMismatch
463 */
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
487 /**
488 * Virtual implode with brackets
489 * @param string $start
490 * @param string $sep
491 * @param string $end
492 * @param string|PPNode_DOM|DOMNode ...$args
493 * @return array
494 * @suppress PhanParamSignatureMismatch
495 */
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
532 /**
533 * @return array
534 */
535 public function getArguments() {
536 return [];
537 }
538
539 /**
540 * @return array
541 */
542 public function getNumberedArguments() {
543 return [];
544 }
545
546 /**
547 * @return array
548 */
549 public function getNamedArguments() {
550 return [];
551 }
552
553 /**
554 * Returns true if there are no arguments in this frame
555 *
556 * @return bool
557 */
558 public function isEmpty() {
559 return true;
560 }
561
562 /**
563 * @param int|string $name
564 * @return bool Always false in this implementation.
565 */
566 public function getArgument( $name ) {
567 return false;
568 }
569
570 /**
571 * Returns true if the infinite loop check is OK, false if a loop is detected
572 *
573 * @param Title $title
574 * @return bool
575 */
576 public function loopCheck( $title ) {
577 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
578 }
579
580 /**
581 * Return true if the frame is a template frame
582 *
583 * @return bool
584 */
585 public function isTemplate() {
586 return false;
587 }
588
589 /**
590 * Get a title of frame
591 *
592 * @return Title
593 */
594 public function getTitle() {
595 return $this->title;
596 }
597
598 /**
599 * Set the volatile flag
600 *
601 * @param bool $flag
602 */
603 public function setVolatile( $flag = true ) {
604 $this->volatile = $flag;
605 }
606
607 /**
608 * Get the volatile flag
609 *
610 * @return bool
611 */
612 public function isVolatile() {
613 return $this->volatile;
614 }
615
616 /**
617 * Set the TTL
618 *
619 * @param int $ttl
620 */
621 public function setTTL( $ttl ) {
622 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
623 $this->ttl = $ttl;
624 }
625 }
626
627 /**
628 * Get the TTL
629 *
630 * @return int|null
631 */
632 public function getTTL() {
633 return $this->ttl;
634 }
635 }