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