Merge "Optimize HashRing to avoid hashing for the common single-location case"
[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 */
27 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
28 class PPFrame_DOM implements PPFrame {
29
30 /**
31 * @var Preprocessor
32 */
33 public $preprocessor;
34
35 /**
36 * @var Parser
37 */
38 public $parser;
39
40 /**
41 * @var Title
42 */
43 public $title;
44 public $titleCache;
45
46 /**
47 * Hashtable listing templates which are disallowed for expansion in this frame,
48 * having been encountered previously in parent frames.
49 */
50 public $loopCheckHash;
51
52 /**
53 * Recursion depth of this frame, top = 0
54 * Note that this is NOT the same as expansion depth in expand()
55 */
56 public $depth;
57
58 private $volatile = false;
59 private $ttl = null;
60
61 /**
62 * @var array
63 */
64 protected $childExpansionCache;
65
66 /**
67 * Construct a new preprocessor frame.
68 * @param Preprocessor $preprocessor The parent preprocessor
69 */
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
80 /**
81 * Create a new child frame
82 * $args is optionally a multi-root PPNode or array containing the template arguments
83 *
84 * @param bool|array $args
85 * @param Title|bool $title
86 * @param int $indexOffset
87 * @return PPTemplateFrame_DOM
88 */
89 public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
90 $namedArgs = [];
91 $numberedArgs = [];
92 if ( $title === false ) {
93 $title = $this->title;
94 }
95 if ( $args !== false ) {
96 $xpath = false;
97 if ( $args instanceof PPNode ) {
98 $args = $args->node;
99 }
100 foreach ( $args as $arg ) {
101 if ( $arg instanceof PPNode ) {
102 $arg = $arg->node;
103 }
104 if ( !$xpath || $xpath->document !== $arg->ownerDocument ) {
105 $xpath = new DOMXPath( $arg->ownerDocument );
106 }
107
108 $nameNodes = $xpath->query( 'name', $arg );
109 $value = $xpath->query( 'value', $arg );
110 if ( $nameNodes->item( 0 )->hasAttributes() ) {
111 // Numbered parameter
112 $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
113 $index = $index - $indexOffset;
114 if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
115 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
116 wfEscapeWikiText( $this->title ),
117 wfEscapeWikiText( $title ),
118 wfEscapeWikiText( $index ) )->text() );
119 $this->parser->addTrackingCategory( 'duplicate-args-category' );
120 }
121 $numberedArgs[$index] = $value->item( 0 );
122 unset( $namedArgs[$index] );
123 } else {
124 // Named parameter
125 $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) );
126 if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
127 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
128 wfEscapeWikiText( $this->title ),
129 wfEscapeWikiText( $title ),
130 wfEscapeWikiText( $name ) )->text() );
131 $this->parser->addTrackingCategory( 'duplicate-args-category' );
132 }
133 $namedArgs[$name] = $value->item( 0 );
134 unset( $numberedArgs[$name] );
135 }
136 }
137 }
138 return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
139 }
140
141 /**
142 * @throws MWException
143 * @param string|int $key
144 * @param string|PPNode_DOM|DOMNode|DOMNodeList $root
145 * @param int $flags
146 * @return string
147 */
148 public function cachedExpand( $key, $root, $flags = 0 ) {
149 // we don't have a parent, so we don't have a cache
150 return $this->expand( $root, $flags );
151 }
152
153 /**
154 * @throws MWException
155 * @param string|PPNode_DOM|DOMNode $root
156 * @param int $flags
157 * @return string
158 */
159 public function expand( $root, $flags = 0 ) {
160 static $expansionDepth = 0;
161 if ( is_string( $root ) ) {
162 return $root;
163 }
164
165 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
166 $this->parser->limitationWarn( 'node-count-exceeded',
167 $this->parser->mPPNodeCount,
168 $this->parser->mOptions->getMaxPPNodeCount()
169 );
170 return '<span class="error">Node-count limit exceeded</span>';
171 }
172
173 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
174 $this->parser->limitationWarn( 'expansion-depth-exceeded',
175 $expansionDepth,
176 $this->parser->mOptions->getMaxPPExpandDepth()
177 );
178 return '<span class="error">Expansion depth limit exceeded</span>';
179 }
180 ++$expansionDepth;
181 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
182 $this->parser->mHighestExpansionDepth = $expansionDepth;
183 }
184
185 if ( $root instanceof PPNode_DOM ) {
186 $root = $root->node;
187 }
188 if ( $root instanceof DOMDocument ) {
189 $root = $root->documentElement;
190 }
191
192 $outStack = [ '', '' ];
193 $iteratorStack = [ false, $root ];
194 $indexStack = [ 0, 0 ];
195
196 while ( count( $iteratorStack ) > 1 ) {
197 $level = count( $outStack ) - 1;
198 $iteratorNode =& $iteratorStack[$level];
199 $out =& $outStack[$level];
200 $index =& $indexStack[$level];
201
202 if ( $iteratorNode instanceof PPNode_DOM ) {
203 $iteratorNode = $iteratorNode->node;
204 }
205
206 if ( is_array( $iteratorNode ) ) {
207 if ( $index >= count( $iteratorNode ) ) {
208 // All done with this iterator
209 $iteratorStack[$level] = false;
210 $contextNode = false;
211 } else {
212 $contextNode = $iteratorNode[$index];
213 $index++;
214 }
215 } elseif ( $iteratorNode instanceof DOMNodeList ) {
216 if ( $index >= $iteratorNode->length ) {
217 // All done with this iterator
218 $iteratorStack[$level] = false;
219 $contextNode = false;
220 } else {
221 $contextNode = $iteratorNode->item( $index );
222 $index++;
223 }
224 } else {
225 // Copy to $contextNode and then delete from iterator stack,
226 // because this is not an iterator but we do have to execute it once
227 $contextNode = $iteratorStack[$level];
228 $iteratorStack[$level] = false;
229 }
230
231 if ( $contextNode instanceof PPNode_DOM ) {
232 $contextNode = $contextNode->node;
233 }
234
235 $newIterator = false;
236
237 if ( $contextNode === false ) {
238 // nothing to do
239 } elseif ( is_string( $contextNode ) ) {
240 $out .= $contextNode;
241 } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) {
242 $newIterator = $contextNode;
243 } elseif ( $contextNode instanceof DOMNode ) {
244 if ( $contextNode->nodeType == XML_TEXT_NODE ) {
245 $out .= $contextNode->nodeValue;
246 } elseif ( $contextNode->nodeName == 'template' ) {
247 # Double-brace expansion
248 $xpath = new DOMXPath( $contextNode->ownerDocument );
249 $titles = $xpath->query( 'title', $contextNode );
250 $title = $titles->item( 0 );
251 $parts = $xpath->query( 'part', $contextNode );
252 if ( $flags & PPFrame::NO_TEMPLATES ) {
253 $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
254 } else {
255 $lineStart = $contextNode->getAttribute( 'lineStart' );
256 $params = [
257 'title' => new PPNode_DOM( $title ),
258 'parts' => new PPNode_DOM( $parts ),
259 'lineStart' => $lineStart ];
260 $ret = $this->parser->braceSubstitution( $params, $this );
261 if ( isset( $ret['object'] ) ) {
262 $newIterator = $ret['object'];
263 } else {
264 $out .= $ret['text'];
265 }
266 }
267 } elseif ( $contextNode->nodeName == 'tplarg' ) {
268 # Triple-brace expansion
269 $xpath = new DOMXPath( $contextNode->ownerDocument );
270 $titles = $xpath->query( 'title', $contextNode );
271 $title = $titles->item( 0 );
272 $parts = $xpath->query( 'part', $contextNode );
273 if ( $flags & PPFrame::NO_ARGS ) {
274 $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
275 } else {
276 $params = [
277 'title' => new PPNode_DOM( $title ),
278 'parts' => new PPNode_DOM( $parts ) ];
279 $ret = $this->parser->argSubstitution( $params, $this );
280 if ( isset( $ret['object'] ) ) {
281 $newIterator = $ret['object'];
282 } else {
283 $out .= $ret['text'];
284 }
285 }
286 } elseif ( $contextNode->nodeName == 'comment' ) {
287 # HTML-style comment
288 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
289 # Not in RECOVER_COMMENTS mode (msgnw) though.
290 if ( ( $this->parser->ot['html']
291 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
292 || ( $flags & PPFrame::STRIP_COMMENTS )
293 ) && !( $flags & PPFrame::RECOVER_COMMENTS )
294 ) {
295 $out .= '';
296 } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
297 # Add a strip marker in PST mode so that pstPass2() can
298 # run some old-fashioned regexes on the result.
299 # Not in RECOVER_COMMENTS mode (extractSections) though.
300 $out .= $this->parser->insertStripItem( $contextNode->textContent );
301 } else {
302 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
303 $out .= $contextNode->textContent;
304 }
305 } elseif ( $contextNode->nodeName == 'ignore' ) {
306 # Output suppression used by <includeonly> etc.
307 # OT_WIKI will only respect <ignore> in substed templates.
308 # The other output types respect it unless NO_IGNORE is set.
309 # extractSections() sets NO_IGNORE and so never respects it.
310 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
311 || ( $flags & PPFrame::NO_IGNORE )
312 ) {
313 $out .= $contextNode->textContent;
314 } else {
315 $out .= '';
316 }
317 } elseif ( $contextNode->nodeName == 'ext' ) {
318 # Extension tag
319 $xpath = new DOMXPath( $contextNode->ownerDocument );
320 $names = $xpath->query( 'name', $contextNode );
321 $attrs = $xpath->query( 'attr', $contextNode );
322 $inners = $xpath->query( 'inner', $contextNode );
323 $closes = $xpath->query( 'close', $contextNode );
324 if ( $flags & PPFrame::NO_TAGS ) {
325 $s = '<' . $this->expand( $names->item( 0 ), $flags );
326 if ( $attrs->length > 0 ) {
327 $s .= $this->expand( $attrs->item( 0 ), $flags );
328 }
329 if ( $inners->length > 0 ) {
330 $s .= '>' . $this->expand( $inners->item( 0 ), $flags );
331 if ( $closes->length > 0 ) {
332 $s .= $this->expand( $closes->item( 0 ), $flags );
333 }
334 } else {
335 $s .= '/>';
336 }
337 $out .= $s;
338 } else {
339 $params = [
340 'name' => new PPNode_DOM( $names->item( 0 ) ),
341 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null,
342 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null,
343 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null,
344 ];
345 $out .= $this->parser->extensionSubstitution( $params, $this );
346 }
347 } elseif ( $contextNode->nodeName == 'h' ) {
348 # Heading
349 $s = $this->expand( $contextNode->childNodes, $flags );
350
351 # Insert a heading marker only for <h> children of <root>
352 # This is to stop extractSections from going over multiple tree levels
353 if ( $contextNode->parentNode->nodeName == 'root' && $this->parser->ot['html'] ) {
354 # Insert heading index marker
355 $headingIndex = $contextNode->getAttribute( 'i' );
356 $titleText = $this->title->getPrefixedDBkey();
357 $this->parser->mHeadings[] = [ $titleText, $headingIndex ];
358 $serial = count( $this->parser->mHeadings ) - 1;
359 $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
360 $count = $contextNode->getAttribute( 'level' );
361 $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
362 $this->parser->mStripState->addGeneral( $marker, '' );
363 }
364 $out .= $s;
365 } else {
366 # Generic recursive expansion
367 $newIterator = $contextNode->childNodes;
368 }
369 } else {
370 throw new MWException( __METHOD__ . ': Invalid parameter type' );
371 }
372
373 if ( $newIterator !== false ) {
374 if ( $newIterator instanceof PPNode_DOM ) {
375 $newIterator = $newIterator->node;
376 }
377 $outStack[] = '';
378 $iteratorStack[] = $newIterator;
379 $indexStack[] = 0;
380 } elseif ( $iteratorStack[$level] === false ) {
381 // Return accumulated value to parent
382 // With tail recursion
383 while ( $iteratorStack[$level] === false && $level > 0 ) {
384 $outStack[$level - 1] .= $out;
385 array_pop( $outStack );
386 array_pop( $iteratorStack );
387 array_pop( $indexStack );
388 $level--;
389 }
390 }
391 }
392 --$expansionDepth;
393 return $outStack[0];
394 }
395
396 /**
397 * @param string $sep
398 * @param int $flags
399 * @param string|PPNode_DOM|DOMNode ...$args
400 * @return string
401 */
402 public function implodeWithFlags( $sep, $flags, ...$args ) {
403 $first = true;
404 $s = '';
405 foreach ( $args as $root ) {
406 if ( $root instanceof PPNode_DOM ) {
407 $root = $root->node;
408 }
409 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
410 $root = [ $root ];
411 }
412 foreach ( $root as $node ) {
413 if ( $first ) {
414 $first = false;
415 } else {
416 $s .= $sep;
417 }
418 $s .= $this->expand( $node, $flags );
419 }
420 }
421 return $s;
422 }
423
424 /**
425 * Implode with no flags specified
426 * This previously called implodeWithFlags but has now been inlined to reduce stack depth
427 *
428 * @param string $sep
429 * @param string|PPNode_DOM|DOMNode ...$args
430 * @return string
431 */
432 public function implode( $sep, ...$args ) {
433 $first = true;
434 $s = '';
435 foreach ( $args as $root ) {
436 if ( $root instanceof PPNode_DOM ) {
437 $root = $root->node;
438 }
439 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
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
454 /**
455 * Makes an object that, when expand()ed, will be the same as one obtained
456 * with implode()
457 *
458 * @param string $sep
459 * @param string|PPNode_DOM|DOMNode ...$args
460 * @return array
461 * @suppress PhanParamSignatureMismatch
462 */
463 public function virtualImplode( $sep, ...$args ) {
464 $out = [];
465 $first = true;
466
467 foreach ( $args as $root ) {
468 if ( $root instanceof PPNode_DOM ) {
469 $root = $root->node;
470 }
471 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
472 $root = [ $root ];
473 }
474 foreach ( $root as $node ) {
475 if ( $first ) {
476 $first = false;
477 } else {
478 $out[] = $sep;
479 }
480 $out[] = $node;
481 }
482 }
483 return $out;
484 }
485
486 /**
487 * Virtual implode with brackets
488 * @param string $start
489 * @param string $sep
490 * @param string $end
491 * @param string|PPNode_DOM|DOMNode ...$args
492 * @return array
493 * @suppress PhanParamSignatureMismatch
494 */
495 public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
496 $out = [ $start ];
497 $first = true;
498
499 foreach ( $args as $root ) {
500 if ( $root instanceof PPNode_DOM ) {
501 $root = $root->node;
502 }
503 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
504 $root = [ $root ];
505 }
506 foreach ( $root as $node ) {
507 if ( $first ) {
508 $first = false;
509 } else {
510 $out[] = $sep;
511 }
512 $out[] = $node;
513 }
514 }
515 $out[] = $end;
516 return $out;
517 }
518
519 public function __toString() {
520 return 'frame{}';
521 }
522
523 public function getPDBK( $level = false ) {
524 if ( $level === false ) {
525 return $this->title->getPrefixedDBkey();
526 } else {
527 return $this->titleCache[$level] ?? false;
528 }
529 }
530
531 /**
532 * @return array
533 */
534 public function getArguments() {
535 return [];
536 }
537
538 /**
539 * @return array
540 */
541 public function getNumberedArguments() {
542 return [];
543 }
544
545 /**
546 * @return array
547 */
548 public function getNamedArguments() {
549 return [];
550 }
551
552 /**
553 * Returns true if there are no arguments in this frame
554 *
555 * @return bool
556 */
557 public function isEmpty() {
558 return true;
559 }
560
561 /**
562 * @param int|string $name
563 * @return bool Always false in this implementation.
564 */
565 public function getArgument( $name ) {
566 return false;
567 }
568
569 /**
570 * Returns true if the infinite loop check is OK, false if a loop is detected
571 *
572 * @param Title $title
573 * @return bool
574 */
575 public function loopCheck( $title ) {
576 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
577 }
578
579 /**
580 * Return true if the frame is a template frame
581 *
582 * @return bool
583 */
584 public function isTemplate() {
585 return false;
586 }
587
588 /**
589 * Get a title of frame
590 *
591 * @return Title
592 */
593 public function getTitle() {
594 return $this->title;
595 }
596
597 /**
598 * Set the volatile flag
599 *
600 * @param bool $flag
601 */
602 public function setVolatile( $flag = true ) {
603 $this->volatile = $flag;
604 }
605
606 /**
607 * Get the volatile flag
608 *
609 * @return bool
610 */
611 public function isVolatile() {
612 return $this->volatile;
613 }
614
615 /**
616 * Set the TTL
617 *
618 * @param int $ttl
619 */
620 public function setTTL( $ttl ) {
621 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
622 $this->ttl = $ttl;
623 }
624 }
625
626 /**
627 * Get the TTL
628 *
629 * @return int|null
630 */
631 public function getTTL() {
632 return $this->ttl;
633 }
634 }