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