Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
17.74% covered (danger)
17.74%
33 / 186
21.43% covered (danger)
21.43%
6 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
Utils
17.74% covered (danger)
17.74%
33 / 186
21.43% covered (danger)
21.43%
6 / 28
2718.93
0.00% covered (danger)
0.00%
0 / 1
 stripParsoidIdPrefix
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 stripNamespace
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isParsoidObjectId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isVoidElement
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 recursiveClone
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 clone
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 lastUniChar
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 isUniWord
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 phpURLEncode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 decodeURI
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 decodeURIComponent
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 extractExtBody
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 isValidOffset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 isValidDSR
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 normalizeNamespaceName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 decodeWtEntities
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 escapeWtEntities
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 escapeHtml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 entityEncodeAll
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 isProtocolValid
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getExtArgInfo
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 parseMediaDimensions
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 validateMediaParam
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getStar
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isLinkTrail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 bcp47ToMwCode
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
6
 mwCodeToBcp47
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
182
 isBcp47CodeEqual
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Utils;
5
6use Psr\Log\LoggerInterface;
7use Wikimedia\Bcp47Code\Bcp47Code;
8use Wikimedia\Bcp47Code\Bcp47CodeValue;
9use Wikimedia\Parsoid\Config\Env;
10use Wikimedia\Parsoid\Core\DomSourceRange;
11use Wikimedia\Parsoid\Core\Sanitizer;
12use Wikimedia\Parsoid\NodeData\DataMw;
13use Wikimedia\Parsoid\Tokens\Token;
14use Wikimedia\Parsoid\Wikitext\Consts;
15
16/**
17 * This file contains general utilities for token transforms.
18 */
19class Utils {
20    /**
21     * Regular expression fragment for matching wikitext comments.
22     * Meant for inclusion in other regular expressions.
23     */
24    // Maintenance note: this is used in /x regexes so all whitespace and # should be escaped
25    public const COMMENT_REGEXP_FRAGMENT = '<!--(?>[\s\S]*?-->)';
26    /** Regular fragment for matching a wikitext comment */
27    public const COMMENT_REGEXP = '/' . self::COMMENT_REGEXP_FRAGMENT . '/';
28
29    /**
30     * Strip Parsoid id prefix from aboutID
31     *
32     * @param string $aboutId aboud ID string
33     * @return string
34     */
35    public static function stripParsoidIdPrefix( string $aboutId ): string {
36        // 'mwt' is the prefix used for new ids
37        return preg_replace( '/^#?mwt/', '', $aboutId );
38    }
39
40    /**
41     * Strip PHP namespace from the fully qualified class name
42     * @param string $className
43     * @return string
44     */
45    public static function stripNamespace( string $className ): string {
46        return preg_replace( '/.*\\\\/', '', $className );
47    }
48
49    /**
50     * Check for Parsoid id prefix in an aboutID string
51     *
52     * @param string $aboutId aboud ID string
53     * @return bool
54     */
55    public static function isParsoidObjectId( string $aboutId ): bool {
56        // 'mwt' is the prefix used for new ids
57        return str_starts_with( $aboutId, '#mwt' );
58    }
59
60    /**
61     * Determine if the named tag is void (can not have content).
62     *
63     * @param string $name tag name
64     * @return bool
65     */
66    public static function isVoidElement( string $name ): bool {
67        return isset( Consts::$HTML['VoidTags'][$name] );
68    }
69
70    /**
71     * recursive deep clones helper function
72     *
73     * @param object $el object
74     * @return object
75     */
76    private static function recursiveClone( $el ) {
77        return self::clone( $el, true );
78    }
79
80    /**
81     * Deep clones by default.
82     * @param object|array $obj arrays or plain objects
83     *    Tokens or DOM nodes shouldn't be passed in.
84     *
85     *    CAVEAT: It looks like debugging methods pass in arrays
86     *    that can have DOM nodes. So, for debugging purposes,
87     *    we handle top-level DOM nodes or DOM nodes embedded in arrays
88     *    But, this will miserably fail if an object embeds a DOM node.
89     *
90     * @param bool $deepClone
91     * @param bool $debug
92     * @return object|array
93     */
94    public static function clone( $obj, $deepClone = true, $debug = false ) {
95        if ( $debug ) {
96            if ( $obj instanceof \DOMNode ) {
97                return $obj->cloneNode( $deepClone );
98            }
99            if ( is_array( $obj ) ) {
100                if ( $deepClone ) {
101                    return array_map(
102                        static function ( $o ) {
103                            return Utils::clone( $o, true, true );
104                        },
105                        $obj
106                    );
107                } else {
108                    return $obj; // Copy-on-write cloning
109                }
110            }
111        }
112
113        if ( !$deepClone && is_object( $obj ) ) {
114            return clone $obj;
115        }
116
117        // FIXME, see T161647
118        // This will fail if $obj is (or embeds) a DOMNode
119        return unserialize( serialize( $obj ) );
120    }
121
122    /**
123     * Extract the last *unicode* character of the string.
124     * This might be more than one byte, if the last character
125     * is non-ASCII.
126     * @param string $str
127     * @param ?int $idx The index *after* the character to extract; defaults
128     *   to the length of $str, which will extract the last character in
129     *   $str.
130     * @return string
131     */
132    public static function lastUniChar( string $str, ?int $idx = null ): string {
133        if ( $idx === null ) {
134            $idx = strlen( $str );
135        } elseif ( $idx <= 0 || $idx > strlen( $str ) ) {
136            return '';
137        }
138        $c = $str[--$idx];
139        while ( ( ord( $c ) & 0xC0 ) === 0x80 ) {
140            $c = $str[--$idx] . $c;
141        }
142        return $c;
143    }
144
145    /**
146     * Return true if the first character in $s is a unicode word character.
147     * @param string $s
148     * @return bool
149     */
150    public static function isUniWord( string $s ): bool {
151        return preg_match( '#^\w#u', $s ) === 1;
152    }
153
154    /**
155     * This should not be used.
156     * @param string $txt URL to encode using PHP encoding
157     * @return string
158     */
159    public static function phpURLEncode( $txt ) {
160        // @phan-suppress-previous-line PhanPluginNeverReturnMethod
161        throw new \BadMethodCallException( 'Use urlencode( $txt ) instead' );
162    }
163
164    /**
165     * Percent-decode only valid UTF-8 characters, leaving other encoded bytes alone.
166     *
167     * Distinct from `decodeURIComponent` in that certain escapes are not decoded,
168     * matching the behavior of JavaScript's decodeURI().
169     *
170     * @see https://www.ecma-international.org/ecma-262/6.0/#sec-decodeuri-encodeduri
171     * @param string $s URI to be decoded
172     * @return string
173     */
174    public static function decodeURI( string $s ): string {
175        // Escape the '%' in sequences for the reserved characters, then use decodeURIComponent.
176        $s = preg_replace( '/%(?=2[346bcfBCF]|3[abdfABDF]|40)/', '%25', $s );
177        return self::decodeURIComponent( $s );
178    }
179
180    /**
181     * Percent-decode only valid UTF-8 characters, leaving other encoded bytes alone.
182     *
183     * @param string $s URI to be decoded
184     * @return string
185     */
186    public static function decodeURIComponent( string $s ): string {
187        // Most of the time we should have valid input
188        $ret = rawurldecode( $s );
189        if ( mb_check_encoding( $ret, 'UTF-8' ) ) {
190            return $ret;
191        }
192
193        // Extract each encoded character and decode it individually
194        return preg_replace_callback(
195            // phpcs:ignore Generic.Files.LineLength.TooLong
196            '/%[0-7][0-9A-F]|%[CD][0-9A-F]%[89AB][0-9A-F]|%E[0-9A-F](?:%[89AB][0-9A-F]){2}|%F[0-4](?:%[89AB][0-9A-F]){3}/i',
197            static function ( $match ) {
198                $ret = rawurldecode( $match[0] );
199                return mb_check_encoding( $ret, 'UTF-8' ) ? $ret : $match[0];
200            }, $s
201        );
202    }
203
204    /**
205     * Extract extension source from the token
206     *
207     * @param Token $token token
208     * @return string
209     */
210    public static function extractExtBody( Token $token ): string {
211        $src = $token->getAttributeV( 'source' );
212        $extTagOffsets = $token->dataParsoid->extTagOffsets;
213        '@phan-var \Wikimedia\Parsoid\Core\DomSourceRange $extTagOffsets';
214        return $extTagOffsets->stripTags( $src );
215    }
216
217    /**
218     * Helper function checks numeric values
219     *
220     * @param ?int $n checks parameters for numeric type and value zero or positive
221     * @return bool
222     */
223    private static function isValidOffset( ?int $n ): bool {
224        return $n !== null && $n >= 0;
225    }
226
227    /**
228     * Basic check if a DOM Source Range (DSR) is valid.
229     *
230     * Clarifications about the "basic validity checks":
231     * - Only checks for underflow, not for overflow.
232     * - Does not verify that start <= end
233     * - Does not verify that openWidth + endWidth <= end - start
234     *   (even so, the values might be invalid because of content)
235     * These would be overkill for our purposes. Given how DSR computation
236     * works in thie codebase, the real scenarios we care about are
237     * non-null / non-negative values since that can happen.
238     *
239     * @param ?DomSourceRange $dsr DSR source range values
240     * @param bool $all Also check the widths of the container tag
241     * @return bool
242     */
243    public static function isValidDSR(
244        ?DomSourceRange $dsr, bool $all = false
245    ): bool {
246        return $dsr !== null &&
247            self::isValidOffset( $dsr->start ) &&
248            self::isValidOffset( $dsr->end ) &&
249            ( !$all || (
250                self::isValidOffset( $dsr->openWidth ) &&
251                self::isValidOffset( $dsr->closeWidth )
252              )
253            );
254    }
255
256    /**
257     * Cannonicalizes a namespace name.
258     *
259     * @param string $name Non-normalized namespace name.
260     * @return string
261     */
262    public static function normalizeNamespaceName( string $name ): string {
263        return strtr( mb_strtolower( $name ), ' ', '_' );
264    }
265
266    /**
267     * Decode HTML5 entities in wikitext.
268     *
269     * NOTE that wikitext only allows semicolon-terminated entities, while
270     * HTML allows a number of "legacy" entities to be decoded without
271     * a terminating semicolon.  This function deliberately does not
272     * decode these HTML-only entity forms.
273     *
274     * @param string $text
275     * @return string
276     */
277    public static function decodeWtEntities( string $text ): string {
278        // Note that HTML5 allows semicolon-less entities which
279        // wikitext does not: in wikitext all entities must end in a
280        // semicolon.
281        // By normalizing before decoding, this routine deliberately
282        // does not decode entity references which are invalid in wikitext
283        // (mostly because they decode to invalid codepoints).