Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
RemexRemoveTagHandler
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 5
600
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 comment
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 validateTag
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
72
 startTag
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
 endTag
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Parser;
4
5use Wikimedia\RemexHtml\Tokenizer\Attributes;
6use Wikimedia\RemexHtml\Tokenizer\PlainAttributes;
7use Wikimedia\RemexHtml\Tokenizer\RelayTokenHandler;
8use Wikimedia\RemexHtml\Tokenizer\TokenHandler;
9
10/**
11 * Helper class for Sanitizer::removeSomeTags().
12 * @internal
13 */
14class RemexRemoveTagHandler extends RelayTokenHandler {
15    /**
16     * @var string The original HTML source string (used for fallback text
17     * when rejecting an HTML tag).
18     */
19    private $source;
20
21    /**
22     * @var array<string,true> Set of HTML tags which can be self-closed.
23     */
24    private $htmlsingle;
25
26    /**
27     * @var array<string,true> Self-closed tags which are on $htmlsingle
28     * but not on $htmlsingleonly will be emitted as an empty element.
29     */
30    private $htmlsingleonly;
31
32    /**
33     * @var array<string,true> Set of allowed HTML open/close tags.
34     */
35    private $htmlelements;
36
37    /** @var ?string */
38    private $commentRegex;
39
40    /**
41     * @var ?callable(Attributes,mixed...):Attributes Callback to mutate or
42     * sanitize attributes.
43     */
44    private $attrCallback;
45
46    /**
47     * @var ?array $args Optional extra arguments to provide to the
48     * $attrCallback.
49     */
50    private $callbackArgs;
51
52    /**
53     * @param TokenHandler $nextHandler Handler to relay accepted tokens.
54     * @param string $source Input source string.
55     * @param array $tagData Information about allowed/rejected tags.
56     * @param ?callable $attrCallback Attribute handler callback.
57     *   The full signature is ?callable(Attributes,mixed...):Attributes
58     * @param ?array $callbackArgs Optional arguments to attribute handler.
59     * @param array $options Associative array of options:
60     *   - commentRegex: If present, allow comments with inner text matching
61     *     the specified regular expression.
62     */
63    public function __construct(
64        TokenHandler $nextHandler,
65        string $source,
66        array $tagData,
67        ?callable $attrCallback,
68        ?array $callbackArgs,
69        array $options
70    ) {
71        parent::__construct( $nextHandler );
72        $this->source = $source;
73        $this->htmlsingle = $tagData['htmlsingle'];
74        $this->htmlsingleonly = $tagData['htmlsingleonly'];
75        $this->htmlelements = $tagData['htmlelements'];
76        $this->attrCallback = $attrCallback;
77        $this->callbackArgs = $callbackArgs ?? [];
78        $this->commentRegex = $options['commentRegex'] ?? null;
79    }
80
81    /**
82     * @inheritDoc
83     */
84    public function comment( $text, $sourceStart, $sourceLength ) {
85        if ( $this->commentRegex !== null && preg_match( $this->commentRegex, $text ) ) {
86            $this->nextHandler->comment( $text, $sourceStart, $sourceLength );
87        }
88    }
89
90    /**
91     * Takes attribute names and values for a tag and the tag name and
92     * validates that the tag is allowed to be present.
93     * This DOES NOT validate the attributes, nor does it validate the
94     * tags themselves. This method only handles the special circumstances
95     * where we may want to allow a tag within content but ONLY when it has
96     * specific attributes set.
97     *
98     * @param string $element
99     * @param Attributes $attrs
100     * @return bool
101     *
102     * @see Sanitizer::validateTag()
103     */
104    private static function validateTag( string $element, Attributes $attrs ): bool {
105        if ( $element == 'meta' || $element == 'link' ) {
106            $params = $attrs->getValues();
107            if ( !isset( $params['itemprop'] ) ) {
108                // <meta> and <link> must have an itemprop="" otherwise they are not valid or safe in content
109                return false;
110            }
111            if ( $element == 'meta' && !isset( $params['content'] ) ) {
112                // <meta> must have a content="" for the itemprop
113                return false;
114            }
115            if ( $element == 'link' && !isset( $params['href'] ) ) {
116                // <link> must have an associated href=""
117                return false;
118            }
119        }
120
121        return true;
122    }
123
124    /**
125     * @inheritDoc
126     */
127    public function startTag( $name, Attributes $attrs, $selfClose, $sourceStart, $sourceLength ) {
128        // Handle a start tag from the tokenizer: either relay it to the
129        // next stage, or re-emit it as raw text.
130
131        $badtag = false;
132        $t = strtolower( $name );
133        if ( isset( $this->htmlelements[$t] ) ) {
134            if ( $this->attrCallback ) {
135                $attrs = ( $this->attrCallback )( $attrs, ...$this->callbackArgs );
136            }
137            if ( $selfClose && !( isset( $this->htmlsingle[$t] ) || isset( $this->htmlsingleonly[$t] ) ) ) {
138                // Remove the self-closing slash, to be consistent with
139                // HTML5 semantics. T134423
140                $selfClose = false;
141            }
142            if ( !self::validateTag( $t, $attrs ) ) {
143                $badtag = true;
144            }
145            $fixedAttrs = Sanitizer::validateTagAttributes( $attrs->getValues(), $t );
146            $attrs = new PlainAttributes( $fixedAttrs );
147            if ( !$badtag ) {
148                if ( $selfClose && !isset( $this->htmlsingleonly[$t] ) ) {
149                    // Interpret self-closing tags as empty tags even when
150                    // HTML5 would interpret them as start tags.  Such input
151                    // is commonly seen on Wikimedia wikis with this intention.
152                    $this->nextHandler->startTag( $name, $attrs, false, $sourceStart, $sourceLength );
153                    $this->nextHandler->endTag( $name, $sourceStart + $sourceLength, 0 );
154                } else {
155                    $this->nextHandler->startTag( $name, $attrs, $selfClose, $sourceStart, $sourceLength );
156                }
157                return;
158            }
159        }
160        // Emit this as a text node instead.
161        $this->nextHandler->characters( $this->source, $sourceStart, $sourceLength, $sourceStart, $sourceLength );
162    }
163
164    /**
165     * @inheritDoc
166     */
167    public function endTag( $name, $sourceStart, $sourceLength ) {
168        // Handle an end tag from the tokenizer: either relay it to the
169        // next stage, or re-emit it as raw text.
170
171        $t = strtolower( $name );
172        if ( isset( $this->htmlelements[$t] ) ) {
173            // This is a good tag, relay it.
174            $this->nextHandler->endTag( $name, $sourceStart, $sourceLength );
175        } else {
176            // Emit this as a text node instead.
177            $this->nextHandler->characters( $this->source, $sourceStart, $sourceLength, $sourceStart, $sourceLength );
178        }
179    }
180
181}