ExtensionXmlParser::parseXml()   A
last analyzed

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
    protected string $distributionImage = '';
66
67
    public function __construct()
68
    {
69
        if (!extension_loaded('xml')) {
70
            throw new \RuntimeException('PHP extension "xml" not loaded', 1622148496);
71
        }
72
    }
73
74
    /**
75
     * Method parses an extensions.xml file.
76
     *
77
     * @param string $file GZIP stream resource
78
     * @throws ExtensionManagerException in case of parse errors
79
     */
80
    public function parseXml($file): void
81
    {
82
        if (PHP_MAJOR_VERSION < 8) {
83
            // @deprecated will be removed as soon as the minimum version of TYPO3 is 8.0
84
            $this->parseWithLegacyResource($file);
85
            return;
86
        }
87
88
        /** @var \XMLParser $parser */
89
        $parser = xml_parser_create();
90
        xml_set_object($parser, $this);
91
92
        // keep original character case of XML document
93
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
94
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
95
        xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'utf-8');
96
        xml_set_element_handler($parser, [$this, 'startElement'], [$this, 'endElement']);
97
        xml_set_character_data_handler($parser, [$this, 'characterData']);
98
        if (!($fp = fopen($file, 'r'))) {
99
            throw $this->createUnableToOpenFileResourceException($file);
100
        }
101
        while ($data = fread($fp, 4096)) {
102
            if (!xml_parse($parser, $data, feof($fp))) {
103
                throw $this->createXmlErrorException($parser, $file);
104
            }
105
        }
106
        xml_parser_free($parser);
107
    }
108
109
    /**
110
     * @throws ExtensionManagerException
111
     * @internal
112
     */
113
    private function parseWithLegacyResource(string $file): void
114
    {
115
        // Store the xml parser resource in when run with PHP <= 7.4
116
        // @deprecated will be removed as soon as the minimum version of TYPO3 is 8.0
117
        $legacyXmlParserResource = xml_parser_create();
118
        xml_set_object($legacyXmlParserResource, $this);
119
        if ($legacyXmlParserResource === null) {
120
            throw new ExtensionManagerException('Unable to create XML parser.', 1342640663);
121
        }
122
        /** @var resource $parser */
123
        $parser = $legacyXmlParserResource;
124
125
        // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
126
        $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
127
128
        // keep original character case of XML document
129
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
130
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
131
        xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'utf-8');
132
        xml_set_element_handler($parser, [$this, 'startElement'], [$this, 'endElement']);
133
        xml_set_character_data_handler($parser, [$this, 'characterData']);
134
        if (!($fp = fopen($file, 'r'))) {
135
            throw $this->createUnableToOpenFileResourceException($file);
136
        }
137
        while ($data = fread($fp, 4096)) {
138
            if (!xml_parse($parser, $data, feof($fp))) {
139
                throw $this->createXmlErrorException($parser, $file);
140
            }
141
        }
142
143
        libxml_disable_entity_loader($previousValueOfEntityLoader);
144
145
        xml_parser_free($parser);
146
    }
147
148
    private function createUnableToOpenFileResourceException(string $file): ExtensionManagerException
149
    {
150
        return new ExtensionManagerException(sprintf('Unable to open file resource %s.', $file), 1342640689);
151
    }
152
153
    private function createXmlErrorException($parser, string $file): ExtensionManagerException
154
    {
155
        return new ExtensionManagerException(
156
            sprintf(
157
                'XML error %s in line %u of file resource %s.',
158
                xml_error_string(xml_get_error_code($parser)),
159
                xml_get_current_line_number($parser),
160
                $file
161
            ),
162
            1342640703
163
        );
164
    }
165
166
    /**
167
     * Method is invoked when parser accesses start tag of an element.
168
     *
169
     * @param resource $parser parser resource
170
     * @param string $elementName element name at parser's current position
171
     * @param array $attrs array of an element's attributes if available
172
     */
173
    protected function startElement($parser, $elementName, $attrs)
174
    {
175
        switch ($elementName) {
176
            case 'extension':
177
                $this->extensionKey = $attrs['extensionkey'];
178
                break;
179
            case 'version':
180
                $this->version = $attrs['version'];
181
                break;
182
            default:
183
                $this->elementData = '';
184
        }
185
    }
186
187
    /**
188
     * Method is invoked when parser accesses end tag of an element.
189
     *
190
     * @param resource $parser parser resource
191
     * @param string $elementName Element name at parser's current position
192
     */
193
    protected function endElement($parser, $elementName)
194
    {
195
        switch ($elementName) {
196
            case 'extension':
197
                $this->resetProperties(true);
198
                break;
199
            case 'version':
200
                $this->notify();
201
                $this->resetProperties();
202
                break;
203
            case 'downloadcounter':
204
                // downloadcounter can be a child node of extension or version
205
                if ($this->version === '') {
206
                    $this->extensionDownloadCounter = (int)$this->elementData;
207
                } else {
208
                    $this->versionDownloadCounter = (int)$this->elementData;
209
                }
210
                break;
211
            case 'title':
212
                $this->title = $this->elementData;
213
                break;
214
            case 'description':
215
                $this->description = $this->elementData;
216
                break;
217
            case 'state':
218
                $this->state = $this->elementData;
219
                break;
220
            case 'reviewstate':
221
                $this->reviewstate = (int)$this->elementData;
222
                break;
223
            case 'category':
224
                $this->category = $this->elementData;
225
                break;
226
            case 'lastuploaddate':
227
                $this->lastuploaddate = (int)$this->elementData;
228
                break;
229
            case 'uploadcomment':
230
                $this->uploadcomment = $this->elementData;
231
                break;
232
            case 'dependencies':
233
                $newDependencies = [];
234
                $dependenciesArray = unserialize($this->elementData, ['allowed_classes' => false]);
235
                if (is_array($dependenciesArray)) {
236
                    foreach ($dependenciesArray as $version) {
237
                        if (!empty($version['kind']) && !empty($version['extensionKey'])) {
238
                            $newDependencies[$version['kind']][$version['extensionKey']] = $version['versionRange'];
239
                        }
240
                    }
241
                }
242
                $this->dependencies = serialize($newDependencies);
243
                break;
244
            case 'authorname':
245
                $this->authorname = $this->elementData;
246
                break;
247
            case 'authoremail':
248
                $this->authoremail = $this->elementData;
249
                break;
250
            case 'authorcompany':
251
                $this->authorcompany = $this->elementData;
252
                break;
253
            case 'ownerusername':
254
                $this->ownerusername = $this->elementData;
255
                break;
256
            case 't3xfilemd5':
257
                $this->t3xfilemd5 = $this->elementData;
258
                break;
259
            case 'documentation_link':
260
                $this->documentationLink = $this->elementData;
261
                break;
262
            case 'distributionImage':
263
                if (preg_match('/^https:\/\/extensions\.typo3\.org[a-zA-Z0-9._\/]+Distribution\.png$/', $this->elementData)) {
264
                    $this->distributionImage = $this->elementData;
265
                }
266
                break;
267
        }
268
    }
269
270
    /**
271
     * Method resets version class properties.
272
     *
273
     * @param bool $resetAll If TRUE, additionally extension properties are reset
274
     */
275
    protected function resetProperties($resetAll = false): void
276
    {
277
        // Resetting at least class property "version" is mandatory as we need to do some magic in
278
        // regards to an extension's and version's child node "downloadcounter"
279
        $this->version = $this->authorcompany = $this->authorname = $this->authoremail = $this->category = $this->dependencies = $this->state = '';
280
        $this->description = $this->ownerusername = $this->t3xfilemd5 = $this->title = $this->uploadcomment = $this->documentationLink = $this->distributionImage = '';
281
        $this->lastuploaddate = $this->reviewstate = $this->versionDownloadCounter = 0;
282
        if ($resetAll) {
283
            $this->extensionKey = '';
284
            $this->extensionDownloadCounter = 0;
285
        }
286
    }
287
288
    /**
289
     * Method is invoked when parser accesses any character other than elements.
290
     *
291
     * @param resource|\XmlParser $parser XmlParser with PHP >= 8
292
     * @param string $data An element's value
293
     */
294
    protected function characterData($parser, string $data)
295
    {
296
        $this->elementData .= $data;
297
    }
298
299
    /**
300
     * Method attaches an observer.
301
     *
302
     * @param \SplObserver $observer an observer to attach
303
     * @see detach()
304
     * @see notify()
305
     */
306
    public function attach(\SplObserver $observer)
307
    {
308
        $this->observers[] = $observer;
309
    }
310
311
    /**
312
     * Method detaches an attached observer
313
     *
314
     * @param \SplObserver $observer an observer to detach
315
     */
316
    public function detach(\SplObserver $observer)
317
    {
318
        $key = array_search($observer, $this->observers, true);
319
        if ($key !== false) {
320
            unset($this->observers[$key]);
321
        }
322
    }
323
324
    /**
325
     * Method notifies attached observers.
326
     */
327
    public function notify()
328
    {
329
        foreach ($this->observers as $observer) {
330
            $observer->update($this);
331
        }
332
    }
333
334
    /**
335
     * Returns download number sum of all extension's versions.
336
     */
337
    public function getAlldownloadcounter(): int
338
    {
339
        return $this->extensionDownloadCounter;
340
    }
341
342
    /**
343
     * Returns company name of extension author.
344
     */
345
    public function getAuthorcompany(): string
346
    {
347
        return $this->authorcompany;
348
    }
349
350
    /**
351
     * Returns e-mail address of extension author.
352
     */
353
    public function getAuthoremail(): string
354
    {
355
        return $this->authoremail;
356
    }
357
358
    /**
359
     * Returns name of extension author.
360
     */
361
    public function getAuthorname(): string
362
    {
363
        return $this->authorname;
364
    }
365
366
    /**
367
     * Returns category of an extension.
368
     */
369
    public function getCategory(): string
370
    {
371
        return $this->category;
372
    }
373
374
    /**
375
     * Returns dependencies of an extension's version as a serialized string
376
     */
377
    public function getDependencies(): string
378
    {
379
        return $this->dependencies;
380
    }
381
382
    /**
383
     * Returns description of an extension's version.
384
     */
385
    public function getDescription(): string
386
    {
387
        return $this->description;
388
    }
389
390
    /**
391
     * Returns download number of an extension's version.
392
     */
393
    public function getDownloadcounter(): int
394
    {
395
        return $this->versionDownloadCounter;
396
    }
397
398
    /**
399
     * Returns key of an extension.
400
     */
401
    public function getExtkey(): string
402
    {
403
        return $this->extensionKey;
404
    }
405
406
    /**
407
     * Returns last uploaddate of an extension's version.
408
     */
409
    public function getLastuploaddate(): int
410
    {
411
        return $this->lastuploaddate;
412
    }
413
414
    /**
415
     * Returns username of extension owner.
416
     */
417
    public function getOwnerusername(): string
418
    {
419
        return $this->ownerusername;
420
    }
421
422
    /**
423
     * Returns review state of an extension's version.
424
     */
425
    public function getReviewstate(): int
426
    {
427
        return $this->reviewstate;
428
    }
429
430
    /**
431
     * Returns state of an extension's version.
432
     */
433
    public function getState(): string
434
    {
435
        return $this->state;
436
    }
437
438
    /**
439
     * Returns t3x file hash of an extension's version.
440
     */
441
    public function getT3xfilemd5(): string
442
    {
443
        return $this->t3xfilemd5;
444
    }
445
446
    /**
447
     * Returns title of an extension's version.
448
     */
449
    public function getTitle(): string
450
    {
451
        return $this->title;
452
    }
453
454
    /**
455
     * Returns extension upload comment.
456
     */
457
    public function getUploadcomment(): string
458
    {
459
        return $this->uploadcomment;
460
    }
461
462
    /**
463
     * Returns version number as unparsed string.
464
     */
465
    public function getVersion(): string
466
    {
467
        return $this->version;
468
    }
469
470
    /**
471
     * Returns documentation link.
472
     */
473
    public function getDocumentationLink(): string
474
    {
475
        return $this->documentationLink;
476
    }
477
478
    /**
479
     * Returns distribution image.
480
     */
481
    public function getDistributionImage(): string
482
    {
483
        return $this->distributionImage;
484
    }
485
}
486