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