Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
42.31% covered (danger)
42.31%
11 / 26
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
XmlScanner
42.31% covered (danger)
42.31%
11 / 26
66.67% covered (warning)
66.67%
2 / 3
80.22
0.00% covered (danger)
0.00%
0 / 1
 getInstance
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 scan
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 mb_str_split
16.67% covered (danger)
16.67%
3 / 18
0.00% covered (danger)
0.00%
0 / 1
95.33
1<?php
2
3namespace PhpOffice\Math\Reader\Security;
4
5use PhpOffice\Math\Exception\SecurityException;
6
7class XmlScanner
8{
9    public static function getInstance(): self
10    {
11        return new self();
12    }
13
14    /**
15     * Scan the XML for use of <!ENTITY to prevent XXE/XEE attacks.
16     */
17    public function scan(string $xml): string
18    {
19        // Don't rely purely on libxml_disable_entity_loader()
20        $searchDoctype = static::mb_str_split('<!DOCTYPE', 1, 'UTF-8');
21        $patternDoctype = '/\0*' . implode('\0*', is_array($searchDoctype) ? $searchDoctype : []) . '\0*/';
22        $searchDoctypeMath = static::mb_str_split('<!DOCTYPE math', 1, 'UTF-8');
23        $patternDoctypeMath = '/\0*' . implode('\0*', is_array($searchDoctypeMath) ? $searchDoctypeMath : []) . '\0*/';
24
25        if (preg_match($patternDoctype, $xml) && !preg_match($patternDoctypeMath, $xml)) {
26            throw new SecurityException('Detected use of ENTITY in XML, loading aborted to prevent XXE/XEE attacks');
27        }
28
29        return $xml;
30    }
31
32    /**
33     * @param string $string
34     * @param int<1, max> $split_length
35     * @param string|null $encoding
36     *
37     * @return array<string>|bool|null
38     */
39    public static function mb_str_split(string $string, int $split_length = 1, ?string $encoding = null)
40    {
41        if (extension_loaded('mbstring')) {
42            if (function_exists('mb_str_split')) {
43                return mb_str_split($string, $split_length, $encoding);
44            }
45        }
46        // @phpstan-ignore-next-line
47        if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) {
48            trigger_error('mb_str_split() expects parameter 1 to be string, ' . \gettype($string) . ' given', \E_USER_WARNING);
49
50            return null;
51        }
52
53        // @phpstan-ignore-next-line
54        if (1 > $split_length = (int) $split_length) {
55            trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING);
56
57            return false;
58        }
59
60        if (null === $encoding) {
61            $encoding = mb_internal_encoding();
62        }
63
64        if ('UTF-8' === $encoding || \in_array(strtoupper($encoding), ['UTF-8', 'UTF8'], true)) {
65            return preg_split("/(.{{$split_length}})/u", $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
66        }
67
68        $result = [];
69        $length = mb_strlen($string, $encoding);
70
71        for ($i = 0; $i < $length; $i += $split_length) {
72            $result[] = mb_substr($string, $i, $split_length, $encoding);
73        }
74
75        return $result;
76    }
77}