Merge "maintenance: Script to rename titles for Unicode uppercasing changes"
[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->mTitle;
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 */
462 public function virtualImplode( $sep, ...$args ) {
463 $out = [];
464 $first = true;
465
466 foreach ( $args as $root ) {
467 if ( $root instanceof PPNode_DOM ) {
468 $root = $root->node;
469 }
470 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
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 $out;
483 }
484
485 /**
486 * Virtual implode with brackets
487 * @param string $start
488 * @param string $sep
489 * @param string $end
490 * @param string|PPNode_DOM|DOMNode ...$args
491 * @return array
492 */
493 public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
494 $out = [ $start ];
495 $first = true;
496
497 foreach ( $args as $root ) {
498 if ( $root instanceof PPNode_DOM ) {
499 $root = $root->node;
500 }
501 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
502 $root = [ $root ];
503 }
504 foreach ( $root as $node ) {
505 if ( $first ) {
506 $first = false;
507 } else {
508 $out[] = $sep;
509 }
510 $out[] = $node;
511 }
512 }
513 $out[] = $end;
514 return $out;
515 }
516
517 public function __toString() {
518 return 'frame{}';
519 }
520
521 public function getPDBK( $level = false ) {
522 if ( $level === false ) {
523 return $this->title->getPrefixedDBkey();
524 } else {
525 return $this->titleCache[$level] ?? false;
526 }
527 }
528
529 /**
530 * @return array
531 */
532 public function getArguments() {
533 return [];
534 }
535
536 /**
537 * @return array
538 */
539 public function getNumberedArguments() {
540 return [];
541 }
542
543 /**
544 * @return array
545 */
546 public function getNamedArguments() {
547 return [];
548 }
549
550 /**
551 * Returns true if there are no arguments in this frame
552 *
553 * @return bool
554 */
555 public function isEmpty() {
556 return true;
557 }
558
559 /**
560 * @param int|string $name
561 * @return bool Always false in this implementation.
562 */
563 public function getArgument( $name ) {
564 return false;
565 }
566
567 /**
568 * Returns true if the infinite loop check is OK, false if a loop is detected
569 *
570 * @param Title $title
571 * @return bool
572 */
573 public function loopCheck( $title ) {
574 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
575 }
576
577 /**
578 * Return true if the frame is a template frame
579 *
580 * @return bool
581 */
582 public function isTemplate() {
583 return false;
584 }
585
586 /**
587 * Get a title of frame
588 *
589 * @return Title
590 */
591 public function getTitle() {
592 return $this->title;
593 }
594
595 /**
596 * Set the volatile flag
597 *
598 * @param bool $flag
599 */
600 public function setVolatile( $flag = true ) {
601 $this->volatile = $flag;
602 }
603
604 /**
605 * Get the volatile flag
606 *
607 * @return bool
608 */
609 public function isVolatile() {
610 return $this->volatile;
611 }
612
613 /**
614 * Set the TTL
615 *
616 * @param int $ttl
617 */
618 public function setTTL( $ttl ) {
619 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
620 $this->ttl = $ttl;
621 }
622 }
623
624 /**
625 * Get the TTL
626 *
627 * @return int|null
628 */
629 public function getTTL() {
630 return $this->ttl;
631 }
632 }