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