Passed
Push — master ( 7288b4...f131ca )
by Adrien
13:07
created

XmlScanner::shutdown()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 3
nc 2
nop 0
crap 12
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader\Security;
4
5
use PhpOffice\PhpSpreadsheet\Reader;
6
7
class XmlScanner
8
{
9
    private string $pattern;
10
11
    /** @var ?callable */
12
    private $callback;
13
14
    public function __construct(string $pattern = '<!DOCTYPE')
15
    {
16
        $this->pattern = $pattern;
17
    }
18
19
    public static function getInstance(Reader\IReader $reader): self
20
    {
21
        $pattern = ($reader instanceof Reader\Html) ? '<!ENTITY' : '<!DOCTYPE';
22
23
        return new self($pattern);
24
    }
25
26
    /**
27 1194
     * @codeCoverageIgnore
28
     *
29 1194
     * @deprecated this has no effect at all and always return false. All usages must be removed.
30
     */
31 1194
    public static function threadSafeLibxmlDisableEntityLoaderAvailability(): bool
32
    {
33
        return false;
34 1194
    }
35 31
36 31
    public function setAdditionalCallback(callable $callback): void
37
    {
38
        $this->callback = $callback;
39
    }
40 1193
41
    private static function forceString(mixed $arg): string
42 1193
    {
43
        return is_string($arg) ? $arg : '';
44 1193
    }
45
46
    private function toUtf8(string $xml): string
47
    {
48
        $pattern = '/encoding="(.*?)"/';
49
        $result = preg_match($pattern, $xml, $matches);
50
        $charset = strtoupper($result ? $matches[1] : 'UTF-8');
51
52
        if ($charset !== 'UTF-8') {
53
            $xml = self::forceString(mb_convert_encoding($xml, 'UTF-8', $charset));
54
55
            $result = preg_match($pattern, $xml, $matches);
56
            $charset = strtoupper($result ? $matches[1] : 'UTF-8');
57
            if ($charset !== 'UTF-8') {
58
                throw new Reader\Exception('Suspicious Double-encoded XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
59
            }
60
        }
61
62
        return $xml;
63
    }
64
65
    /**
66
     * Scan the XML for use of <!ENTITY to prevent XXE/XEE attacks.
67
     *
68
     * @param false|string $xml
69
     */
70
    public function scan($xml): string
71
    {
72
        $xml = "$xml";
73
74
        $xml = $this->toUtf8($xml);
75
76
        // Don't rely purely on libxml_disable_entity_loader()
77
        $pattern = '/\\0?' . implode('\\0?', /** @scrutinizer ignore-type */ str_split($this->pattern)) . '\\0?/';
78
79
        if (preg_match($pattern, $xml)) {
80
            throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
81
        }
82
83
        if ($this->callback !== null) {
84
            $xml = call_user_func($this->callback, $xml);
85
        }
86
87
        return $xml;
88
    }
89
90
    /**
91
     * Scan theXML for use of <!ENTITY to prevent XXE/XEE attacks.
92
     */
93 1194
    public function scanFile(string $filestream): string
94
    {
95 1194
        return $this->scan(file_get_contents($filestream));
96
    }
97
}
98