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 16namespace TYPO3\CMS\Extensionmanager\Utility; 17 18use TYPO3\CMS\Core\SingletonInterface; 19use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; 20use TYPO3\CMS\Core\Utility\GeneralUtility; 21use TYPO3\CMS\Core\Utility\VersionNumberUtility; 22use TYPO3\CMS\Extensionmanager\Domain\Model\Dependency; 23use TYPO3\CMS\Extensionmanager\Domain\Model\Extension; 24use TYPO3\CMS\Extensionmanager\Domain\Repository\ExtensionRepository; 25use TYPO3\CMS\Extensionmanager\Exception; 26use TYPO3\CMS\Extensionmanager\Exception\MissingExtensionDependencyException; 27use TYPO3\CMS\Extensionmanager\Exception\MissingVersionDependencyException; 28use TYPO3\CMS\Extensionmanager\Exception\UnresolvedDependencyException; 29use TYPO3\CMS\Extensionmanager\Exception\UnresolvedPhpDependencyException; 30use TYPO3\CMS\Extensionmanager\Exception\UnresolvedTypo3DependencyException; 31use TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService; 32 33/** 34 * Utility for dealing with dependencies 35 * @internal This class is a specific ExtensionManager implementation and is not part of the Public TYPO3 API. 36 */ 37class DependencyUtility implements SingletonInterface 38{ 39 /** 40 * @var ExtensionRepository 41 */ 42 protected $extensionRepository; 43 44 /** 45 * @var ListUtility 46 */ 47 protected $listUtility; 48 49 /** 50 * @var EmConfUtility 51 */ 52 protected $emConfUtility; 53 54 /** 55 * @var ExtensionManagementService 56 */ 57 protected $managementService; 58 59 /** 60 * @var array 61 */ 62 protected $availableExtensions = []; 63 64 /** 65 * @var string 66 */ 67 protected $localExtensionStorage = ''; 68 69 /** 70 * @var array 71 */ 72 protected $dependencyErrors = []; 73 74 /** 75 * @var bool 76 */ 77 protected $skipDependencyCheck = false; 78 79 /** 80 * @param ExtensionRepository $extensionRepository 81 */ 82 public function injectExtensionRepository(ExtensionRepository $extensionRepository) 83 { 84 $this->extensionRepository = $extensionRepository; 85 } 86 87 /** 88 * @param ListUtility $listUtility 89 */ 90 public function injectListUtility(ListUtility $listUtility) 91 { 92 $this->listUtility = $listUtility; 93 } 94 95 /** 96 * @param EmConfUtility $emConfUtility 97 */ 98 public function injectEmConfUtility(EmConfUtility $emConfUtility) 99 { 100 $this->emConfUtility = $emConfUtility; 101 } 102 103 /** 104 * @param ExtensionManagementService $managementService 105 */ 106 public function injectManagementService(ExtensionManagementService $managementService) 107 { 108 $this->managementService = $managementService; 109 } 110 111 /** 112 * @param string $localExtensionStorage 113 */ 114 public function setLocalExtensionStorage($localExtensionStorage) 115 { 116 $this->localExtensionStorage = $localExtensionStorage; 117 } 118 119 /** 120 * Setter for available extensions 121 * gets available extensions from list utility if not already done 122 */ 123 protected function setAvailableExtensions() 124 { 125 $this->availableExtensions = $this->listUtility->getAvailableExtensions(); 126 } 127 128 /** 129 * @param bool $skipDependencyCheck 130 */ 131 public function setSkipDependencyCheck($skipDependencyCheck) 132 { 133 $this->skipDependencyCheck = $skipDependencyCheck; 134 } 135 136 /** 137 * Checks dependencies for special cases (currently typo3 and php) 138 * 139 * @param Extension $extension 140 */ 141 public function checkDependencies(Extension $extension) 142 { 143 $this->dependencyErrors = []; 144 $dependencies = $extension->getDependencies(); 145 foreach ($dependencies as $dependency) { 146 /** @var Dependency $dependency */ 147 $identifier = strtolower($dependency->getIdentifier()); 148 try { 149 if (in_array($identifier, Dependency::$specialDependencies)) { 150 if (!$this->skipDependencyCheck) { 151 $methodName = 'check' . ucfirst($identifier) . 'Dependency'; 152 $this->{$methodName}($dependency); 153 } 154 } else { 155 if ($dependency->getType() === 'depends') { 156 $this->checkExtensionDependency($dependency); 157 } 158 } 159 } catch (UnresolvedDependencyException $e) { 160 if (in_array($identifier, Dependency::$specialDependencies)) { 161 $extensionKey = $extension->getExtensionKey(); 162 } else { 163 $extensionKey = $identifier; 164 } 165 if (!isset($this->dependencyErrors[$extensionKey])) { 166 $this->dependencyErrors[$extensionKey] = []; 167 } 168 $this->dependencyErrors[$extensionKey][] = [ 169 'code' => $e->getCode(), 170 'message' => $e->getMessage() 171 ]; 172 } 173 } 174 } 175 176 /** 177 * Returns TRUE if a dependency error was found 178 * 179 * @return bool 180 */ 181 public function hasDependencyErrors() 182 { 183 return !empty($this->dependencyErrors); 184 } 185 186 /** 187 * Return the dependency errors 188 * 189 * @return array 190 */ 191 public function getDependencyErrors() 192 { 193 return $this->dependencyErrors; 194 } 195 196 /** 197 * Returns true if current TYPO3 version fulfills extension requirements 198 * 199 * @param Dependency $dependency 200 * @throws Exception\UnresolvedTypo3DependencyException 201 * @return bool 202 */ 203 protected function checkTypo3Dependency(Dependency $dependency) 204 { 205 $lowerCaseIdentifier = strtolower($dependency->getIdentifier()); 206 if ($lowerCaseIdentifier === 'typo3') { 207 if (!($dependency->getLowestVersion() === '') && version_compare(VersionNumberUtility::getNumericTypo3Version(), $dependency->getLowestVersion()) === -1) { 208 throw new UnresolvedTypo3DependencyException( 209 'Your TYPO3 version is lower than this extension requires. It requires TYPO3 versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(), 210 1399144499 211 ); 212 } 213 if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), VersionNumberUtility::getNumericTypo3Version()) === -1) { 214 throw new UnresolvedTypo3DependencyException( 215 'Your TYPO3 version is higher than this extension requires. It requires TYPO3 versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(), 216 1399144521 217 ); 218 } 219 } else { 220 throw new UnresolvedTypo3DependencyException( 221 'checkTypo3Dependency can only check TYPO3 dependencies. Found dependency with identifier "' . $dependency->getIdentifier() . '"', 222 1399144551 223 ); 224 } 225 return true; 226 } 227 228 /** 229 * Returns true if current php version fulfills extension requirements 230 * 231 * @param Dependency $dependency 232 * @throws Exception\UnresolvedPhpDependencyException 233 * @return bool 234 */ 235 protected function checkPhpDependency(Dependency $dependency) 236 { 237 $lowerCaseIdentifier = strtolower($dependency->getIdentifier()); 238 if ($lowerCaseIdentifier === 'php') { 239 if (!($dependency->getLowestVersion() === '') && version_compare(PHP_VERSION, $dependency->getLowestVersion()) === -1) { 240 throw new UnresolvedPhpDependencyException( 241 'Your PHP version is lower than necessary. You need at least PHP version ' . $dependency->getLowestVersion(), 242 1377977857 243 ); 244 } 245 if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), PHP_VERSION) === -1) { 246 throw new UnresolvedPhpDependencyException( 247 'Your PHP version is higher than allowed. You can use PHP versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(), 248 1377977856 249 ); 250 } 251 } else { 252 throw new UnresolvedPhpDependencyException( 253 'checkPhpDependency can only check PHP dependencies. Found dependency with identifier "' . $dependency->getIdentifier() . '"', 254 1377977858 255 ); 256 } 257 return true; 258 } 259 260 /** 261 * Main controlling function for checking dependencies 262 * Dependency check is done in the following way: 263 * - installed extension in matching version ? - return true 264 * - available extension in matching version ? - mark for installation 265 * - remote (TER) extension in matching version? - mark for download 266 * 267 * @todo handle exceptions / markForUpload 268 * @param Dependency $dependency 269 * @throws Exception\MissingVersionDependencyException 270 * @return bool 271 */ 272 protected function checkExtensionDependency(Dependency $dependency) 273 { 274 $extensionKey = $dependency->getIdentifier(); 275 $extensionIsLoaded = $this->isDependentExtensionLoaded($extensionKey); 276 if ($extensionIsLoaded === true) { 277 $isLoadedVersionCompatible = $this->isLoadedVersionCompatible($dependency); 278 if ($isLoadedVersionCompatible === true || $this->skipDependencyCheck) { 279 return true; 280 } 281 $extension = $this->listUtility->getExtension($extensionKey); 282 $loadedVersion = $extension->getPackageMetaData()->getVersion(); 283 if (version_compare($loadedVersion, $dependency->getHighestVersion()) === -1) { 284 try { 285 $this->getExtensionFromRepository($extensionKey, $dependency); 286 } catch (UnresolvedDependencyException $e) { 287 throw new MissingVersionDependencyException( 288 'The extension ' . $extensionKey . ' is installed in version ' . $loadedVersion 289 . ' but needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion() . ' and could not be fetched from TER', 290 1396302624 291 ); 292 } 293 } else { 294 throw new MissingVersionDependencyException( 295 'The extension ' . $extensionKey . ' is installed in version ' . $loadedVersion . 296 ' but needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(), 297 1430561927 298 ); 299 } 300 } else { 301 $extensionIsAvailable = $this->isDependentExtensionAvailable($extensionKey); 302 if ($extensionIsAvailable === true) { 303 $isAvailableVersionCompatible = $this->isAvailableVersionCompatible($dependency); 304 if ($isAvailableVersionCompatible) { 305 $unresolvedDependencyErrors = $this->dependencyErrors; 306 $this->managementService->markExtensionForInstallation($extensionKey); 307 $this->dependencyErrors = array_merge($unresolvedDependencyErrors, $this->dependencyErrors); 308 } else { 309 $extension = $this->listUtility->getExtension($extensionKey); 310 $availableVersion = $extension->getPackageMetaData()->getVersion(); 311 if (version_compare($availableVersion, $dependency->getHighestVersion()) === -1) { 312 try { 313 $this->getExtensionFromRepository($extensionKey, $dependency); 314 } catch (MissingExtensionDependencyException $e) { 315 if (!$this->skipDependencyCheck) { 316 throw new MissingVersionDependencyException( 317 'The extension ' . $extensionKey . ' is available in version ' . $availableVersion 318 . ' but is needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion() . ' and could not be fetched from TER', 319 1430560390 320 ); 321 } 322 } 323 } else { 324 if (!$this->skipDependencyCheck) { 325 throw new MissingVersionDependencyException( 326 'The extension ' . $extensionKey . ' is available in version ' . $availableVersion 327 . ' but is needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(), 328 1430562374 329 ); 330 } 331 // Dependency check is skipped and the local version has to be installed 332 $this->managementService->markExtensionForInstallation($extensionKey); 333 } 334 } 335 } else { 336 $unresolvedDependencyErrors = $this->dependencyErrors; 337 $this->getExtensionFromRepository($extensionKey, $dependency); 338 $this->dependencyErrors = array_merge($unresolvedDependencyErrors, $this->dependencyErrors); 339 } 340 } 341 342 return false; 343 } 344 345 /** 346 * Get an extension from a repository 347 * (might be in the extension itself or the TER) 348 * 349 * @param string $extensionKey 350 * @param Dependency $dependency 351 * @throws Exception\UnresolvedDependencyException 352 */ 353 protected function getExtensionFromRepository($extensionKey, Dependency $dependency) 354 { 355 if (!$this->getExtensionFromInExtensionRepository($extensionKey)) { 356 $this->getExtensionFromTer($extensionKey, $dependency); 357 } 358 } 359 360 /** 361 * Gets an extension from the in extension repository 362 * (the local extension storage) 363 * 364 * @param string $extensionKey 365 * @return bool 366 */ 367 protected function getExtensionFromInExtensionRepository($extensionKey) 368 { 369 if ($this->localExtensionStorage !== '' && is_dir($this->localExtensionStorage)) { 370 $extList = GeneralUtility::get_dirs($this->localExtensionStorage); 371 $extList = is_array($extList) ? $extList : []; 372 if (in_array($extensionKey, $extList)) { 373 $this->managementService->markExtensionForCopy($extensionKey, $this->localExtensionStorage); 374 return true; 375 } 376 } 377 return false; 378 } 379 380 /** 381 * Handles checks to find a compatible extension version from TER to fulfill given dependency 382 * 383 * @todo unit tests 384 * @param string $extensionKey 385 * @param Dependency $dependency 386 * @throws Exception\UnresolvedDependencyException 387 */ 388 protected function getExtensionFromTer($extensionKey, Dependency $dependency) 389 { 390 $isExtensionDownloadableFromTer = $this->isExtensionDownloadableFromTer($extensionKey); 391 if (!$isExtensionDownloadableFromTer) { 392 if (!$this->skipDependencyCheck) { 393 if ($this->extensionRepository->countAll() > 0) { 394 throw new MissingExtensionDependencyException( 395 'The extension ' . $extensionKey . ' is not available from TER.', 396 1399161266 397 ); 398 } 399 throw new MissingExtensionDependencyException( 400 'The extension ' . $extensionKey . ' could not be checked. Please update your Extension-List from TYPO3 Extension Repository (TER).', 401 1430580308 402 ); 403 } 404 return; 405 } 406 407 $isDownloadableVersionCompatible = $this->isDownloadableVersionCompatible($dependency); 408 if (!$isDownloadableVersionCompatible) { 409 if (!$this->skipDependencyCheck) { 410 throw new MissingVersionDependencyException( 411 'No compatible version found for extension ' . $extensionKey, 412 1399161284 413 ); 414 } 415 return; 416 } 417 418 $latestCompatibleExtensionByIntegerVersionDependency = $this->getLatestCompatibleExtensionByIntegerVersionDependency($dependency); 419 if (!$latestCompatibleExtensionByIntegerVersionDependency instanceof Extension) { 420 if (!$this->skipDependencyCheck) { 421 throw new MissingExtensionDependencyException( 422 'Could not resolve dependency for "' . $dependency->getIdentifier() . '"', 423 1399161302 424 ); 425 } 426 return; 427 } 428 429 if ($this->isDependentExtensionLoaded($extensionKey)) { 430 $this->managementService->markExtensionForUpdate($latestCompatibleExtensionByIntegerVersionDependency); 431 } else { 432 $this->managementService->markExtensionForDownload($latestCompatibleExtensionByIntegerVersionDependency); 433 } 434 } 435 436 /** 437 * @param string $extensionKey 438 * @return bool 439 */ 440 protected function isDependentExtensionLoaded($extensionKey) 441 { 442 return ExtensionManagementUtility::isLoaded($extensionKey); 443 } 444 445 /** 446 * @param Dependency $dependency 447 * @return bool 448 */ 449 protected function isLoadedVersionCompatible(Dependency $dependency) 450 { 451 $extensionVersion = ExtensionManagementUtility::getExtensionVersion($dependency->getIdentifier()); 452 return $this->isVersionCompatible($extensionVersion, $dependency); 453 } 454 455 /** 456 * @param string $version 457 * @param Dependency $dependency 458 * @return bool 459 */ 460 protected function isVersionCompatible($version, Dependency $dependency) 461 { 462 if (!($dependency->getLowestVersion() === '') && version_compare($version, $dependency->getLowestVersion()) === -1) { 463 return false; 464 } 465 if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), $version) === -1) { 466 return false; 467 } 468 return true; 469 } 470 471 /** 472 * Checks whether the needed extension is available 473 * (not necessarily installed, but present in system) 474 * 475 * @param string $extensionKey 476 * @return bool 477 */ 478 protected function isDependentExtensionAvailable($extensionKey) 479 { 480 $this->setAvailableExtensions(); 481 return array_key_exists($extensionKey, $this->availableExtensions); 482 } 483 484 /** 485 * Checks whether the available version is compatible 486 * 487 * @param Dependency $dependency 488 * @return bool 489 */ 490 protected function isAvailableVersionCompatible(Dependency $dependency) 491 { 492 $this->setAvailableExtensions(); 493 $extensionData = $this->emConfUtility->includeEmConf( 494 $dependency->getIdentifier(), 495 $this->availableExtensions[$dependency->getIdentifier()] 496 ); 497 return $this->isVersionCompatible($extensionData['version'], $dependency); 498 } 499 500 /** 501 * Checks whether a ter extension with $extensionKey exists 502 * 503 * @param string $extensionKey 504 * @return bool 505 */ 506 protected function isExtensionDownloadableFromTer($extensionKey) 507 { 508 return $this->extensionRepository->countByExtensionKey($extensionKey) > 0; 509 } 510 511 /** 512 * Checks whether a compatible version of the extension exists in TER 513 * 514 * @param Dependency $dependency 515 * @return bool 516 */ 517 protected function isDownloadableVersionCompatible(Dependency $dependency) 518 { 519 $versions = $this->getLowestAndHighestIntegerVersions($dependency); 520 $count = $this->extensionRepository->countByVersionRangeAndExtensionKey( 521 $dependency->getIdentifier(), 522 $versions['lowestIntegerVersion'], 523 $versions['highestIntegerVersion'] 524 ); 525 return !empty($count); 526 } 527 528 /** 529 * Get the latest compatible version of an extension that's 530 * compatible with the current core and PHP version. 531 * 532 * @param iterable $extensions 533 * @return Extension|null 534 */ 535 protected function getCompatibleExtension(iterable $extensions): ?Extension 536 { 537 foreach ($extensions as $extension) { 538 /** @var Extension $extension */ 539 $this->checkDependencies($extension); 540 $extensionKey = $extension->getExtensionKey(); 541 542 if (isset($this->dependencyErrors[$extensionKey])) { 543 // reset dependencyErrors and continue with next version 544 unset($this->dependencyErrors[$extensionKey]); 545 continue; 546 } 547 548 return $extension; 549 } 550 551 return null; 552 } 553 554 /** 555 * Get the latest compatible version of an extension that 556 * fulfills the given dependency from TER 557 * 558 * @param Dependency $dependency 559 * @return Extension 560 */ 561 protected function getLatestCompatibleExtensionByIntegerVersionDependency(Dependency $dependency) 562 { 563 $versions = $this->getLowestAndHighestIntegerVersions($dependency); 564 $compatibleDataSets = $this->extensionRepository->findByVersionRangeAndExtensionKeyOrderedByVersion( 565 $dependency->getIdentifier(), 566 $versions['lowestIntegerVersion'], 567 $versions['highestIntegerVersion'] 568 ); 569 return $this->getCompatibleExtension($compatibleDataSets); 570 } 571 572 /** 573 * Return array of lowest and highest version of dependency as integer 574 * 575 * @param Dependency $dependency 576 * @return array 577 */ 578 protected function getLowestAndHighestIntegerVersions(Dependency $dependency) 579 { 580 $lowestVersion = $dependency->getLowestVersion(); 581 $lowestVersionInteger = $lowestVersion ? VersionNumberUtility::convertVersionNumberToInteger($lowestVersion) : 0; 582 $highestVersion = $dependency->getHighestVersion(); 583 $highestVersionInteger = $highestVersion ? VersionNumberUtility::convertVersionNumberToInteger($highestVersion) : 0; 584 return [ 585 'lowestIntegerVersion' => $lowestVersionInteger, 586 'highestIntegerVersion' => $highestVersionInteger 587 ]; 588 } 589 590 /** 591 * @param string $extensionKey 592 * @return array 593 */ 594 public function findInstalledExtensionsThatDependOnMe($extensionKey) 595 { 596 $availableAndInstalledExtensions = $this->listUtility->getAvailableAndInstalledExtensionsWithAdditionalInformation(); 597 $dependentExtensions = []; 598 foreach ($availableAndInstalledExtensions as $availableAndInstalledExtensionKey => $availableAndInstalledExtension) { 599 if (isset($availableAndInstalledExtension['installed']) && $availableAndInstalledExtension['installed'] === true) { 600 if (is_array($availableAndInstalledExtension['constraints']) && is_array($availableAndInstalledExtension['constraints']['depends']) && array_key_exists($extensionKey, $availableAndInstalledExtension['constraints']['depends'])) { 601 $dependentExtensions[] = $availableAndInstalledExtensionKey; 602 } 603 } 604 } 605 return $dependentExtensions; 606 } 607 608 /** 609 * Get extensions (out of a given list) that are suitable for the current TYPO3 version 610 * 611 * @param \TYPO3\CMS\Extbase\Persistence\QueryResultInterface|array $extensions List of extensions to check 612 * @return array List of extensions suitable for current TYPO3 version 613 */ 614 public function getExtensionsSuitableForTypo3Version($extensions) 615 { 616 $suitableExtensions = []; 617 /** @var Extension $extension */ 618 foreach ($extensions as $extension) { 619 /** @var Dependency $dependency */ 620 foreach ($extension->getDependencies() as $dependency) { 621 if ($dependency->getIdentifier() === 'typo3') { 622 try { 623 if ($this->checkTypo3Dependency($dependency)) { 624 $suitableExtensions[] = $extension; 625 } 626 } catch (UnresolvedTypo3DependencyException $e) { 627 } 628 break; 629 } 630 } 631 } 632 return $suitableExtensions; 633 } 634 635 /** 636 * Gets a list of various extensions in various versions and returns 637 * a filtered list containing the extension-version combination with 638 * the highest version number. 639 * 640 * @param Extension[] $extensions 641 * @param bool $showUnsuitable 642 * 643 * @return Extension[] 644 */ 645 public function filterYoungestVersionOfExtensionList(array $extensions, $showUnsuitable) 646 { 647 if (!$showUnsuitable) { 648 $extensions = $this->getExtensionsSuitableForTypo3Version($extensions); 649 } 650 $filteredExtensions = []; 651 foreach ($extensions as $extension) { 652 $extensionKey = $extension->getExtensionKey(); 653 if (!array_key_exists($extensionKey, $filteredExtensions)) { 654 $filteredExtensions[$extensionKey] = $extension; 655 continue; 656 } 657 $currentVersion = $filteredExtensions[$extensionKey]->getVersion(); 658 $newVersion = $extension->getVersion(); 659 if (version_compare($newVersion, $currentVersion, '>')) { 660 $filteredExtensions[$extensionKey] = $extension; 661 } 662 } 663 return $filteredExtensions; 664 } 665} 666