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