|
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(); |
|
|
|
|
|
|
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
|
|
|
|
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
$accountIdthat can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theidproperty of an instance of theAccountclass. 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.