Failed Conditions
Push — master ( 1e7115...0e6238 )
by Mark
25:51
created

XmlScanner::__destruct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader\Security;
4
5
use PhpOffice\PhpSpreadsheet\Reader;
6
use PhpOffice\PhpSpreadsheet\Settings;
7
8
class XmlScanner
9
{
10
    /**
11
     * Identifies whether the thread-safe libxmlDisableEntityLoader() function is available.
12
     *
13
     * @var bool
14
     */
15
    private $libxmlDisableEntityLoader = false;
0 ignored issues
show
introduced by
The private property $libxmlDisableEntityLoader is not used, and could be removed.
Loading history...
16
17
    /**
18
     * String used to identify risky xml elements.
19
     *
20
     * @var string
21
     */
22
    private $pattern;
23
24
    private $callback;
25
26
    private static $libxmlDisableEntityLoaderValue;
27
28 128
    public function __construct($pattern = '<!DOCTYPE')
29
    {
30 128
        $this->pattern = $pattern;
31
32 128
        $this->disableEntityLoaderCheck();
33
34
        // A fatal error will bypass the destructor, so we register a shutdown here
35 128
        register_shutdown_function([$this, '__destruct']);
36 128
    }
37
38 128
    public static function getInstance(Reader\IReader $reader)
39
    {
40
        switch (true) {
41 128
            case $reader instanceof Reader\Html:
42 26
                return new self('<!ENTITY');
43 102
            case $reader instanceof Reader\Xlsx:
44 48
            case $reader instanceof Reader\Xml:
45 21
            case $reader instanceof Reader\Ods:
46 4
            case $reader instanceof Reader\Gnumeric:
47 102
                return new self('<!DOCTYPE');
48
            default:
49
                return new self('<!DOCTYPE');
50
        }
51
    }
52
53
    public static function threadSafeLibxmlDisableEntityLoaderAvailability()
54
    {
55
        if (PHP_MAJOR_VERSION == 7) {
56
            switch (PHP_MINOR_VERSION) {
57
                case 2:
58
                    return PHP_RELEASE_VERSION >= 1;
59
                case 1:
60
                    return PHP_RELEASE_VERSION >= 13;
61
                case 0:
62
                    return PHP_RELEASE_VERSION >= 27;
63
            }
64
65
            return true;
66
        }
67
68
        return false;
69
    }
70
71 128
    private function disableEntityLoaderCheck()
72
    {
73 128
        if (Settings::getLibXmlDisableEntityLoader()) {
74 128
            $libxmlDisableEntityLoaderValue = libxml_disable_entity_loader(true);
75
76 128
            if (self::$libxmlDisableEntityLoaderValue === null) {
77 18
                self::$libxmlDisableEntityLoaderValue = $libxmlDisableEntityLoaderValue;
78
            }
79
        }
80 128
    }
81
82
    private function shutdown()
83
    {
84
        if (self::$libxmlDisableEntityLoaderValue !== null) {
85
            libxml_disable_entity_loader(self::$libxmlDisableEntityLoaderValue);
86
        }
87
    }
88
89
    public function __destruct()
90
    {
91
        $this->shutdown();
92
    }
93
94 1
    public function setAdditionalCallback(callable $callback)
95
    {
96 1
        $this->callback = $callback;
97 1
    }
98
99 108
    private function toUtf8($xml)
100
    {
101 108
        $pattern = '/encoding="(.*?)"/';
102 108
        $result = preg_match($pattern, $xml, $matches);
103 108
        $charset = $result ? $matches[1] : 'UTF-8';
104
105 108
        if ($charset !== 'UTF-8') {
106 4
            $xml = mb_convert_encoding($xml, 'UTF-8', $charset);
107
108 4
            $result = preg_match($pattern, $xml, $matches);
109 4
            $charset = $result ? $matches[1] : 'UTF-8';
110 4
            if ($charset !== 'UTF-8') {
111 4
                throw new Reader\Exception('Suspicious Double-encoded XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
112
            }
113
        }
114
115 104
        return $xml;
116
    }
117
118
    /**
119
     * Scan the XML for use of <!ENTITY to prevent XXE/XEE attacks.
120
     *
121
     * @param mixed $xml
122
     *
123
     * @throws Reader\Exception
124
     *
125
     * @return string
126
     */
127 108
    public function scan($xml)
128
    {
129 108
        $this->disableEntityLoaderCheck();
130
131 108
        $xml = $this->toUtf8($xml);
132
133
        // Don't rely purely on libxml_disable_entity_loader()
134 104
        $pattern = '/\\0?' . implode('\\0?', str_split($this->pattern)) . '\\0?/';
135
136 104
        if (preg_match($pattern, $xml)) {
137 8
            throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
138
        }
139
140 96
        if ($this->callback !== null && is_callable($this->callback)) {
141 1
            $xml = call_user_func($this->callback, $xml);
142
        }
143
144 96
        return $xml;
145
    }
146
147
    /**
148
     * Scan theXML for use of <!ENTITY to prevent XXE/XEE attacks.
149
     *
150
     * @param string $filestream
151
     *
152
     * @throws Reader\Exception
153
     *
154
     * @return string
155
     */
156 40
    public function scanFile($filestream)
157
    {
158 40
        return $this->scan(file_get_contents($filestream));
159
    }
160
}
161