Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.73% covered (warning)
75.73%
78 / 103
60.00% covered (warning)
60.00%
9 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
StripState
76.47% covered (warning)
76.47%
78 / 102
60.00% covered (warning)
60.00%
9 / 15
47.19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 addNoWiki
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addGeneral
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addExtTag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addItem
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 unstripGeneral
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 unstripNoWiki
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 replaceNoWikis
73.33% covered (warning)
73.33%
11 / 15
0.00% covered (danger)
0.00%
0 / 1
5.47
 split
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 unstripBoth
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 unstripType
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
8
 getLimitationWarning
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getWarning
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getLimitReport
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 killMarkers
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Holder for stripped items when parsing wiki markup.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Parser
22 */
23
24namespace MediaWiki\Parser;
25
26use Closure;
27use InvalidArgumentException;
28
29/**
30 * @todo document, briefly.
31 * @newable
32 * @ingroup Parser
33 */
34class StripState {
35    /** @var array[] */
36    protected $data;
37    /** @var string */
38    protected $regex;
39
40    protected ?Parser $parser;
41
42    /** @var array */
43    protected $circularRefGuard;
44    /** @var int */
45    protected $depth = 0;
46    /** @var int */
47    protected $highestDepth = 0;
48    /** @var int */
49    protected $expandSize = 0;
50
51    /** @var int */
52    protected $depthLimit = 20;
53    /** @var int */
54    protected $sizeLimit = 5_000_000;
55
56    /**
57     * @stable to call
58     *
59     * @param Parser|null $parser
60     * @param array $options
61     */
62    public function __construct( ?Parser $parser = null, $options = [] ) {
63        $this->data = [
64            'nowiki' => [],
65            'general' => []
66        ];
67        $this->regex = '/' . Parser::MARKER_PREFIX . "([^\x7f<>&'\"]+)" . Parser::MARKER_SUFFIX . '/';
68        $this->circularRefGuard = [];
69        $this->parser = $parser;
70
71        if ( isset( $options['depthLimit'] ) ) {
72            $this->depthLimit = $options['depthLimit'];
73        }
74        if ( isset( $options['sizeLimit'] ) ) {
75            $this->sizeLimit = $options['sizeLimit'];
76        }
77    }
78
79    /**
80     * Add a nowiki strip item
81     * @param string $marker
82     * @param string|Closure $value
83     */
84    public function addNoWiki( $marker, $value ) {
85        $this->addItem( 'nowiki', $marker, $value );
86    }
87
88    /**
89     * @param string $marker
90     * @param string|Closure $value
91     */
92    public function addGeneral( $marker, $value ) {
93        $this->addItem( 'general', $marker, $value );
94    }
95
96    /**
97     * @param string $marker
98     * @param string|Closure $value
99     * @since 1.44
100     * @internal Parsoid use only.
101     */
102    public function addExtTag( $marker, $value ) {
103        $this->addItem( 'exttag', $marker, $value );
104    }
105
106    /**
107     * @param string $type
108     * @param-taint $type none
109     * @param string $marker
110     * @param-taint $marker none
111     * @param string|Closure $value
112     * @param-taint $value exec_html
113     */
114    protected function addItem( $type, $marker, $value ) {
115        if ( !preg_match( $this->regex, $marker, $m ) ) {
116            throw new InvalidArgumentException( "Invalid marker: $marker" );
117        }
118
119        $this->data[$type][$m[1]] = $value;
120    }
121
122    /**
123     * @param string $text
124     * @return mixed
125     */
126    public function unstripGeneral( $text ) {
127        return $this->unstripType( 'general', $text );
128    }
129
130    /**
131     * @param string $text
132     * @return mixed
133     */
134    public function unstripNoWiki( $text ) {
135        return $this->unstripType( 'nowiki', $text );
136    }
137
138    /**
139     * @param string $text
140     * @param callable $callback
141     * @return string
142     */
143    public function replaceNoWikis( string $text, callable $callback ): string {
144        // Shortcut
145        if ( !count( $this->data['nowiki'] ) ) {
146            return $text;
147        }
148
149        $callback = function ( $m ) use ( $callback ) {
150            $marker = $m[1];
151            if ( isset( $this->data['nowiki'][$marker] ) ) {
152                $value = $this->data['nowiki'][$marker];
153                if ( $value instanceof Closure ) {
154                    $value = $value();
155                }
156
157                $this->expandSize += strlen( $value );
158                if ( $this->expandSize > $this->sizeLimit ) {
159                    return $this->getLimitationWarning( 'unstrip-size', $this->sizeLimit );
160                }
161
162                return call_user_func( $callback, $value );
163            } else {
164                return $m[0];
165            }
166        };
167
168        return preg_replace_callback( $this->regex, $callback, $text );
169    }
170
171    /**
172     * Split the given text by strip markers, returning an array that
173     * alternates between plain text and strip marker information.  The
174     * strip marker information includes 'type', and 'content'.  The
175     * resulting array will always be at least 1 element long and contain
176     * an odd number of elements.
177     * @return array<string|array{type:string,content:string}>
178     */
179    public function split( string $text ): array {
180        $pieces = preg_split( $this->regex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
181        for ( $i = 1; $i < count( $pieces ); $i += 2 ) {
182            $marker = $pieces[$i];
183            foreach ( $this->data as $type => $items ) {
184                if ( isset( $items[$marker] ) ) {
185                    $pieces[$i] = [
186                        'type' => $type,
187                        'content' => $items[$marker],
188                    ];
189                    continue 2;
190                }
191            }
192            $pieces[$i] = [
193                'marker' => $marker,
194                'type' => 'unknown',
195                'content' => null,
196            ];
197        }
198        return $pieces;
199    }
200
201    /**
202     * @param string $text
203     * @return mixed
204     */
205    public function unstripBoth( $text ) {
206        $text = $this->unstripType( 'general', $text );
207        $text = $this->unstripType( 'nowiki', $text );
208        return $text;
209    }
210
211    /**
212     * @param string $type
213     * @param string $text
214     * @return mixed
215     */
216    protected function unstripType( $type, $text ) {
217        // Shortcut
218        if ( !count( $this->data[$type] ) ) {
219            return $text;
220        }
221
222        $callback = function ( $m ) use ( $type ) {
223            $marker = $m[1];
224            if ( isset( $this->data[$type][$marker] ) ) {
225                if ( isset( $this->circularRefGuard[$marker] ) ) {
226                    return $this->getWarning( 'parser-unstrip-loop-warning' );
227                }
228
229                if ( $this->depth > $this->highestDepth ) {
230                    $this->highestDepth = $this->depth;
231                }
232                if ( $this->depth >= $this->depthLimit ) {
233                    return $this->getLimitationWarning( 'unstrip-depth', $this->depthLimit );
234                }
235
236                $value = $this->data[$type][$marker];
237                if ( $value instanceof Closure ) {
238                    $value = $value();
239                }
240
241                $this->expandSize += strlen( $value );
242                if ( $this->expandSize > $this->sizeLimit ) {
243                    return $this->getLimitationWarning( 'unstrip-size', $this->sizeLimit );
244                }
245
246                $this->circularRefGuard[$marker] = true;
247                $this->depth++;
248                $ret = $this->unstripType( $type, $value );
249                $this->depth--;
250                unset( $this->circularRefGuard[$marker] );
251
252                return $ret;
253            } else {
254                return $m[0];
255            }
256        };
257
258        $text = preg_replace_callback( $this->regex, $callback, $text );
259        return $text;
260    }
261
262    /**
263     * Get warning HTML and register a limitation warning with the parser
264     *
265     * @param string $type
266     * @param int|string $max
267     * @return string
268     */
269    private function getLimitationWarning( $type, $max = '' ) {
270        if ( $this->parser ) {
271            $this->parser->limitationWarn( $type, $max );
272        }
273        return $this->getWarning( "$type-warning", $max );
274    }
275
276    /**
277     * Get warning HTML
278     *
279     * @param string $message
280     * @param int|string $max
281     * @return string
282     */
283    private function getWarning( $message, $max = '' ) {
284        return '<span class="error">' .
285            wfMessage( $message )
286                ->numParams( $max )->inContentLanguage()->text() .
287            '</span>';
288    }
289
290    /**
291     * Get an array of parameters to pass to ParserOutput::setLimitReportData()
292     *
293     * @internal Should only be called by Parser
294     * @return array
295     */
296    public function getLimitReport() {
297        return [
298            [ 'limitreport-unstrip-depth',
299                [
300                    $this->highestDepth,
301                    $this->depthLimit
302                ],
303            ],
304            [ 'limitreport-unstrip-size',
305                [
306                    $this->expandSize,
307                    $this->sizeLimit
308                ],
309            ]
310        ];
311    }
312
313    /**
314     * Remove any strip markers found in the given text.
315     *
316     * @param string $text
317     * @return string
318     */
319    public function killMarkers( $text ) {
320        return preg_replace( $this->regex, '', $text );
321    }
322}
323
324/** @deprecated class alias since 1.43 */
325class_alias( StripState::class, 'StripState' );