Passed
Push — master ( b8dac1...9827a7 )
by
unknown
24:45 queued 10:04
created

createUnableToOpenFileResourceException()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Extensionmanager\Utility\Parser;
17
18
use TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException;
19
20
/**
21
 * Parser for TYPO3's extension.xml file.
22
 *
23
 * Depends on PHP ext/xml which should be available
24
 * with PHP 4+. This is the parser used in TYPO3
25
 * Core <= 4.3 (without the "collect all data in one
26
 * array" behaviour).
27
 * Notice: ext/xml has proven to be buggy with entities.
28
 * Use at least PHP 5.2.9+ and libxml2 2.7.3+!
29
 * @internal This class is a specific ExtensionManager implementation and is not part of the Public TYPO3 API.
30
 */
31
class ExtensionXmlPushParser extends AbstractExtensionXmlParser
32
{
33
    /**
34
     * Property to store the xml parser resource in when run with PHP <= 7.4
35
     *
36
     * @var resource|null
37
     * @deprecated will be removed as soon as the minimum version of TYPO3 is 8.0
38
     */
39
    protected $legacyXmlParserResource;
40
41
    /**
42
     * Property to store the xml parser resource in when run with PHP >= 8.0
43
     */
44
    protected ?\XMLParser $xmlParser = null;
45
46
    /**
47
     * Keeps current data of element to process.
48
     *
49
     * @var string
50
     */
51
    protected $elementData = '';
52
53
    /**
54
     * Class constructor.
55
     */
56
    public function __construct()
57
    {
58
        $this->requiredPhpExtensions = 'xml';
59
        $this->createParser();
60
    }
61
62
    /**
63
     * Create required parser
64
     */
65
    protected function createParser()
66
    {
67
        if (PHP_MAJOR_VERSION >= 8) {
68
            $this->xmlParser = xml_parser_create();
69
            xml_set_object($this->xmlParser, $this);
70
        } else {
71
            $this->legacyXmlParserResource = xml_parser_create();
0 ignored issues
show
Documentation Bug introduced by
It seems like xml_parser_create() can also be of type XmlParser. However, the property $legacyXmlParserResource is declared as type null|resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
72
            xml_set_object($this->legacyXmlParserResource, $this);
73
        }
74
    }
75
76
    /**
77
     * Method parses an extensions.xml file.
78
     *
79
     * @param string $file GZIP stream resource
80
     * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException in case of parse errors
81
     */
82
    public function parseXml($file)
83
    {
84
        $this->createParser();
85
        if (PHP_MAJOR_VERSION < 8) {
86
            $this->parseWithLegacyResource($file);
87
            return;
88
        }
89
90
        if ($this->xmlParser === null) {
91
            throw $this->createUnableToCreateXmlParseException();
92
        }
93
94
        /** @var \XMLParser $parser */
95
        $parser = $this->xmlParser;
96
97
        // keep original character case of XML document
98
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
99
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
100
        xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'utf-8');
101
        xml_set_element_handler($parser, [$this, 'startElement'], [$this, 'endElement']);
102
        xml_set_character_data_handler($parser, [$this, 'characterData']);
103
        if (!($fp = fopen($file, 'r'))) {
104
            throw $this->createUnableToOpenFileResourceException($file);
105
        }
106
        while ($data = fread($fp, 4096)) {
107
            if (!xml_parse($parser, $data, feof($fp))) {
108
                throw $this->createXmlErrorException($parser, $file);
109
            }
110
        }
111
        xml_parser_free($parser);
112
    }
113
114
    /**
115
     * @throws ExtensionManagerException
116
     * @internal
117
     */
118
    private function parseWithLegacyResource(string $file)
119
    {
120
        if ($this->legacyXmlParserResource === null) {
121
            throw $this->createUnableToCreateXmlParseException();
122
        }
123
124
        /** @var resource $parser */
125
        $parser = $this->legacyXmlParserResource;
126
127
        // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
128
        $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
129
130
        // keep original character case of XML document
131
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
132
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
133
        xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'utf-8');
134
        xml_set_element_handler($parser, [$this, 'startElement'], [$this, 'endElement']);
135
        xml_set_character_data_handler($parser, [$this, 'characterData']);
136
        if (!($fp = fopen($file, 'r'))) {
137
            throw $this->createUnableToOpenFileResourceException($file);
138
        }
139
        while ($data = fread($fp, 4096)) {
140
            if (!xml_parse($parser, $data, feof($fp))) {
141
                throw $this->createXmlErrorException($parser, $file);
142
            }
143
        }
144
145
        libxml_disable_entity_loader($previousValueOfEntityLoader);
146
147
        xml_parser_free($parser);
148
    }
149
150
    private function createUnableToCreateXmlParseException(): ExtensionManagerException
151
    {
152
        return new ExtensionManagerException('Unable to create XML parser.', 1342640663);
153
    }
154
155
    private function createUnableToOpenFileResourceException(string $file): ExtensionManagerException
156
    {
157
        return new ExtensionManagerException(sprintf('Unable to open file resource %s.', $file), 1342640689);
158
    }
159
160
    private function createXmlErrorException($parser, string $file): ExtensionManagerException
161
    {
162
        return new ExtensionManagerException(
163
            sprintf(
164
                'XML error %s in line %u of file resource %s.',
165
                xml_error_string(xml_get_error_code($parser)),
166
                xml_get_current_line_number($parser),
167
                $file
168
            ),
169
            1342640703
170
        );
171
    }
172
173
    /**
174
     * Method is invoked when parser accesses start tag of an element.
175
     *
176
     * @param resource $parser parser resource
177
     * @param string $elementName element name at parser's current position
178
     * @param array $attrs array of an element's attributes if available
179
     */
180
    protected function startElement($parser, $elementName, $attrs)
181
    {
182
        switch ($elementName) {
183
            case 'extension':
184
                $this->extensionKey = $attrs['extensionkey'];
185
                break;
186
            case 'version':
187
                $this->version = $attrs['version'];
188
                break;
189
            default:
190
                $this->elementData = '';
191
        }
192
    }
193
194
    /**
195
     * Method is invoked when parser accesses end tag of an element.
196
     *
197
     * @param resource $parser parser resource
198
     * @param string $elementName Element name at parser's current position
199
     */
200
    protected function endElement($parser, $elementName)
201
    {
202
        switch ($elementName) {
203
            case 'extension':
204
                $this->resetProperties(true);
205
                break;
206
            case 'version':
207
                $this->notify();
208
                $this->resetProperties();
209
                break;
210
            case 'downloadcounter':
211
                // downloadcounter could be a child node of
212
                // extension or version
213
                if ($this->version == null) {
214
                    $this->extensionDownloadCounter = $this->elementData;
215
                } else {
216
                    $this->versionDownloadCounter = $this->elementData;
217
                }
218
                break;
219
            case 'title':
220
                $this->title = $this->elementData;
221
                break;
222
            case 'description':
223
                $this->description = $this->elementData;
224
                break;
225
            case 'state':
226
                $this->state = $this->elementData;
227
                break;
228
            case 'reviewstate':
229
                $this->reviewstate = $this->elementData;
230
                break;
231
            case 'category':
232
                $this->category = $this->elementData;
233
                break;
234
            case 'lastuploaddate':
235
                $this->lastuploaddate = $this->elementData;
236
                break;
237
            case 'uploadcomment':
238
                $this->uploadcomment = $this->elementData;
239
                break;
240
            case 'dependencies':
241
                $this->dependencies = $this->convertDependencies($this->elementData);
242
                break;
243
            case 'authorname':
244
                $this->authorname = $this->elementData;
245
                break;
246
            case 'authoremail':
247
                $this->authoremail = $this->elementData;
248
                break;
249
            case 'authorcompany':
250
                $this->authorcompany = $this->elementData;
251
                break;
252
            case 'ownerusername':
253
                $this->ownerusername = $this->elementData;
254
                break;
255
            case 't3xfilemd5':
256
                $this->t3xfilemd5 = $this->elementData;
257
                break;
258
            case 'documentation_link':
259
                $this->documentationLink = $this->elementData;
260
                break;
261
        }
262
    }
263
264
    /**
265
     * Method is invoked when parser accesses any character other than elements.
266
     *
267
     * @param resource $parser parser resource
268
     * @param string $data An element's value
269
     */
270
    protected function characterData($parser, $data)
271
    {
272
        $this->elementData .= $data;
273
    }
274
}
275