Passed
Push — master ( 9c9dec...a73d8f )
by
unknown
15:09
created

ExtensionXmlParser::parseXml()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 16
dl 0
loc 27
rs 9.4222
c 1
b 0
f 0
cc 5
nc 5
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Extensionmanager\Parser;
19
20
use TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException;
21
22
/**
23
 * Parser for TYPO3's extension.xml file.
24
 *
25
 * Depends on PHP ext/xml which is a required composer php extension
26
 * and enabled in PHP by default since a long time.
27
 *
28
 * @internal This class is a specific ExtensionManager implementation and is not part of the Public TYPO3 API.
29
 */
30
class ExtensionXmlParser implements \SplSubject
31
{
32
    /**
33
     * Keeps list of attached observers.
34
     *
35
     * @var \SplObserver[]
36
     */
37
    protected array $observers = [];
38
39
    /**
40
     * Keeps current data of element to process.
41
     */
42
    protected string $elementData = '';
43
44
    /**
45
     * Parsed property data
46
     */
47
    protected string $authorcompany = '';
48
    protected string $authoremail = '';
49
    protected string $authorname = '';
50
    protected string $category = '';
51
    protected string $dependencies = '';
52
    protected string $description = '';
53
    protected int $extensionDownloadCounter = 0;
54
    protected string $extensionKey = '';
55
    protected int $lastuploaddate = 0;
56
    protected string $ownerusername = '';
57
    protected int $reviewstate = 0;
58
    protected string $state = '';
59
    protected string $t3xfilemd5 = '';
60
    protected string $title = '';
61
    protected string $uploadcomment = '';
62
    protected string $version = '';
63
    protected int $versionDownloadCounter = 0;
64
    protected string $documentationLink = '';
65
66
    public function __construct()
67
    {
68
        if (!extension_loaded('xml')) {
69
            throw new \RuntimeException('PHP extension "xml" not loaded', 1622148496);
70
        }
71
    }
72
73
    /**
74
     * Method parses an extensions.xml file.
75
     *
76
     * @param string $file GZIP stream resource
77
     * @throws ExtensionManagerException in case of parse errors
78
     */
79
    public function parseXml($file): void
80
    {
81
        if (PHP_MAJOR_VERSION < 8) {
82
            // @deprecated will be removed as soon as the minimum version of TYPO3 is 8.0
83
            $this->parseWithLegacyResource($file);
84
            return;
85
        }
86
87
        /** @var \XMLParser $parser */
88
        $parser = xml_parser_create();
89
        xml_set_object($parser, $this);
90
91
        // keep original character case of XML document
92
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
93
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
94
        xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'utf-8');
95
        xml_set_element_handler($parser, [$this, 'startElement'], [$this, 'endElement']);
96
        xml_set_character_data_handler($parser, [$this, 'characterData']);
97
        if (!($fp = fopen($file, 'r'))) {
98
            throw $this->createUnableToOpenFileResourceException($file);
99
        }
100
        while ($data = fread($fp, 4096)) {
101
            if (!xml_parse($parser, $data, feof($fp))) {
102
                throw $this->createXmlErrorException($parser, $file);
103
            }
104
        }
105
        xml_parser_free($parser);
106
    }
107
108
    /**
109
     * @throws ExtensionManagerException
110
     * @internal
111
     */
112
    private function parseWithLegacyResource(string $file): void
113
    {
114
        // Store the xml parser resource in when run with PHP <= 7.4
115
        // @deprecated will be removed as soon as the minimum version of TYPO3 is 8.0
116
        $legacyXmlParserResource = xml_parser_create();
117
        xml_set_object($legacyXmlParserResource, $this);
118
        if ($legacyXmlParserResource === null) {
119
            throw new ExtensionManagerException('Unable to create XML parser.', 1342640663);
120
        }
121
        /** @var resource $parser */
122
        $parser = $legacyXmlParserResource;
123
124
        // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
125
        $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
126
127
        // keep original character case of XML document
128
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
129
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
130
        xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'utf-8');
131
        xml_set_element_handler($parser, [$this, 'startElement'], [$this, 'endElement']);
132
        xml_set_character_data_handler($parser, [$this, 'characterData']);
133
        if (!($fp = fopen($file, 'r'))) {
134
            throw $this->createUnableToOpenFileResourceException($file);
135
        }
136
        while ($data = fread($fp, 4096)) {
137
            if (!xml_parse($parser, $data, feof($fp))) {
138
                throw $this->createXmlErrorException($parser, $file);
139
            }
140
        }
141
142
        libxml_disable_entity_loader($previousValueOfEntityLoader);
143
144
        xml_parser_free($parser);
145
    }
146
147
    private function createUnableToOpenFileResourceException(string $file): ExtensionManagerException
148
    {
149
        return new ExtensionManagerException(sprintf('Unable to open file resource %s.', $file), 1342640689);
150
    }
151
152
    private function createXmlErrorException($parser, string $file): ExtensionManagerException
153
    {
154
        return new ExtensionManagerException(
155
            sprintf(
156
                'XML error %s in line %u of file resource %s.',
157
                xml_error_string(xml_get_error_code($parser)),
158
                xml_get_current_line_number($parser),
159
                $file
160
            ),
161
            1342640703
162
        );
163
    }
164
165
    /**
166
     * Method is invoked when parser accesses start tag of an element.
167
     *
168
     * @param resource $parser parser resource
169
     * @param string $elementName element name at parser's current position
170
     * @param array $attrs array of an element's attributes if available
171
     */
172
    protected function startElement($parser, $elementName, $attrs)
173
    {
174
        switch ($elementName) {
175
            case 'extension':
176
                $this->extensionKey = $attrs['extensionkey'];
177
                break;
178
            case 'version':
179
                $this->version = $attrs['version'];
180
                break;
181
            default:
182
                $this->elementData = '';
183
        }
184
    }
185
186
    /**
187
     * Method is invoked when parser accesses end tag of an element.
188
     *
189
     * @param resource $parser parser resource
190
     * @param string $elementName Element name at parser's current position
191
     */
192
    protected function endElement($parser, $elementName)
193
    {
194
        switch ($elementName) {
195
            case 'extension':
196
                $this->resetProperties(true);
197
                break;
198
            case 'version':
199
                $this->notify();
200
                $this->resetProperties();
201
                break;
202
            case 'downloadcounter':
203
                // downloadcounter can be a child node of extension or version
204
                if ($this->version === '') {
205
                    $this->extensionDownloadCounter = (int)$this->elementData;
206
                } else {
207
                    $this->versionDownloadCounter = (int)$this->elementData;
208
                }
209
                break;
210
            case 'title':
211
                $this->title = $this->elementData;
212
                break;
213
            case 'description':
214
                $this->description = $this->elementData;
215
                break;
216
            case 'state':
217
                $this->state = $this->elementData;
218
                break;
219
            case 'reviewstate':
220
                $this->reviewstate = (int)$this->elementData;
221
                break;
222
            case 'category':
223
                $this->category = $this->elementData;
224
                break;
225
            case 'lastuploaddate':
226
                $this->lastuploaddate = (int)$this->elementData;
227
                break;
228
            case 'uploadcomment':
229
                $this->uploadcomment = $this->elementData;
230
                break;
231
            case 'dependencies':
232
                $newDependencies = [];
233
                $dependenciesArray = unserialize($this->elementData, ['allowed_classes' => false]);
234
                if (is_array($dependenciesArray)) {
235
                    foreach ($dependenciesArray as $version) {
236
                        if (!empty($version['kind']) && !empty($version['extensionKey'])) {
237
                            $newDependencies[$version['kind']][$version['extensionKey']] = $version['versionRange'];
238
                        }
239
                    }
240
                }
241
                $this->dependencies = serialize($newDependencies);
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 resets version class properties.
266
     *
267
     * @param bool $resetAll If TRUE, additionally extension properties are reset
268
     */
269
    protected function resetProperties($resetAll = false): void
270
    {
271
        // Resetting at least class property "version" is mandatory as we need to do some magic in
272
        // regards to an extension's and version's child node "downloadcounter"
273
        $this->version = $this->authorcompany = $this->authorname = $this->authoremail = $this->category = $this->dependencies = $this->state = '';
274
        $this->description = $this->ownerusername = $this->t3xfilemd5 = $this->title = $this->uploadcomment = $this->documentationLink = '';
275
        $this->lastuploaddate = $this->reviewstate = $this->versionDownloadCounter = 0;
276
        if ($resetAll) {
277
            $this->extensionKey = '';
278
            $this->extensionDownloadCounter = 0;
279
        }
280
    }
281
282
    /**
283
     * Method is invoked when parser accesses any character other than elements.
284
     *
285
     * @param resource|\XmlParser $parser XmlParser with PHP >= 8
286
     * @param string $data An element's value
287
     */
288
    protected function characterData($parser, string $data)
289
    {
290
        $this->elementData .= $data;
291
    }
292
293
    /**
294
     * Method attaches an observer.
295
     *
296
     * @param \SplObserver $observer an observer to attach
297
     * @see detach()
298
     * @see notify()
299
     */
300
    public function attach(\SplObserver $observer)
301
    {
302
        $this->observers[] = $observer;
303
    }
304
305
    /**
306
     * Method detaches an attached observer
307
     *
308
     * @param \SplObserver $observer an observer to detach
309
     */
310
    public function detach(\SplObserver $observer)
311
    {
312
        $key = array_search($observer, $this->observers, true);
313
        if ($key !== false) {
314
            unset($this->observers[$key]);
315
        }
316
    }
317
318
    /**
319
     * Method notifies attached observers.
320
     */
321
    public function notify()
322
    {
323
        foreach ($this->observers as $observer) {
324
            $observer->update($this);
325
        }
326
    }
327
328
    /**
329
     * Returns download number sum of all extension's versions.
330
     */
331
    public function getAlldownloadcounter(): int
332
    {
333
        return $this->extensionDownloadCounter;
334
    }
335
336
    /**
337
     * Returns company name of extension author.
338
     */
339
    public function getAuthorcompany(): string
340
    {
341
        return $this->authorcompany;
342
    }
343
344
    /**
345
     * Returns e-mail address of extension author.
346
     */
347
    public function getAuthoremail(): string
348
    {
349
        return $this->authoremail;
350
    }
351
352
    /**
353
     * Returns name of extension author.
354
     */
355
    public function getAuthorname(): string
356
    {
357
        return $this->authorname;
358
    }
359
360
    /**
361
     * Returns category of an extension.
362
     */
363
    public function getCategory(): string
364
    {
365
        return $this->category;
366
    }
367
368
    /**
369
     * Returns dependencies of an extension's version as a serialized string
370
     */
371
    public function getDependencies(): string
372
    {
373
        return $this->dependencies;
374
    }
375
376
    /**
377
     * Returns description of an extension's version.
378
     */
379
    public function getDescription(): string
380
    {
381
        return $this->description;
382
    }
383
384
    /**
385
     * Returns download number of an extension's version.
386
     */
387
    public function getDownloadcounter(): int
388
    {
389
        return $this->versionDownloadCounter;
390
    }
391
392
    /**
393
     * Returns key of an extension.
394
     */
395
    public function getExtkey(): string
396
    {
397
        return $this->extensionKey;
398
    }
399
400
    /**
401
     * Returns last uploaddate of an extension's version.
402
     */
403
    public function getLastuploaddate(): int
404
    {
405
        return $this->lastuploaddate;
406
    }
407
408
    /**
409
     * Returns username of extension owner.
410
     */
411
    public function getOwnerusername(): string
412
    {
413
        return $this->ownerusername;
414
    }
415
416
    /**
417
     * Returns review state of an extension's version.
418
     */
419
    public function getReviewstate(): int
420
    {
421
        return $this->reviewstate;
422
    }
423
424
    /**
425
     * Returns state of an extension's version.
426
     */
427
    public function getState(): string
428
    {
429
        return $this->state;
430
    }
431
432
    /**
433
     * Returns t3x file hash of an extension's version.
434
     */
435
    public function getT3xfilemd5(): string
436
    {
437
        return $this->t3xfilemd5;
438
    }
439
440
    /**
441
     * Returns title of an extension's version.
442
     */
443
    public function getTitle(): string
444
    {
445
        return $this->title;
446
    }
447
448
    /**
449
     * Returns extension upload comment.
450
     */
451
    public function getUploadcomment(): string
452
    {
453
        return $this->uploadcomment;
454
    }
455
456
    /**
457
     * Returns version number as unparsed string.
458
     */
459
    public function getVersion(): string
460
    {
461
        return $this->version;
462
    }
463
464
    /**
465
     * Returns documentation link.
466
     */
467
    public function getDocumentationLink(): string
468
    {
469
        return $this->documentationLink;
470
    }
471
}
472