objectManager = $objectManager; } /** * @param \TYPO3\CMS\Extensionmanager\Domain\Repository\ExtensionRepository $extensionRepository */ public function injectExtensionRepository(\TYPO3\CMS\Extensionmanager\Domain\Repository\ExtensionRepository $extensionRepository) { $this->extensionRepository = $extensionRepository; } /** * @param \TYPO3\CMS\Extensionmanager\Utility\ListUtility $listUtility */ public function injectListUtility(\TYPO3\CMS\Extensionmanager\Utility\ListUtility $listUtility) { $this->listUtility = $listUtility; } /** * @param \TYPO3\CMS\Extensionmanager\Utility\EmConfUtility $emConfUtility */ public function injectEmConfUtility(\TYPO3\CMS\Extensionmanager\Utility\EmConfUtility $emConfUtility) { $this->emConfUtility = $emConfUtility; } /** * @param \TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService $managementService */ public function injectManagementService(\TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService $managementService) { $this->managementService = $managementService; } /** * @param string $localExtensionStorage */ public function setLocalExtensionStorage($localExtensionStorage) { $this->localExtensionStorage = $localExtensionStorage; } /** * Setter for available extensions * gets available extensions from list utility if not already done */ protected function setAvailableExtensions() { $this->availableExtensions = $this->listUtility->getAvailableExtensions(); } /** * @param bool $skipDependencyCheck */ public function setSkipDependencyCheck($skipDependencyCheck) { $this->skipDependencyCheck = $skipDependencyCheck; } /** * Checks dependencies for special cases (currently typo3 and php) * * @param Extension $extension */ public function checkDependencies(Extension $extension) { $this->dependencyErrors = []; $dependencies = $extension->getDependencies(); foreach ($dependencies as $dependency) { /** @var Dependency $dependency */ $identifier = strtolower($dependency->getIdentifier()); try { if (in_array($identifier, Dependency::$specialDependencies)) { if (!$this->skipDependencyCheck) { $methodName = 'check' . ucfirst($identifier) . 'Dependency'; $this->{$methodName}($dependency); } } else { if ($dependency->getType() === 'depends') { $this->checkExtensionDependency($dependency); } } } catch (Exception\UnresolvedDependencyException $e) { if (in_array($identifier, Dependency::$specialDependencies)) { $extensionKey = $extension->getExtensionKey(); } else { $extensionKey = $identifier; } if (!isset($this->dependencyErrors[$extensionKey])) { $this->dependencyErrors[$extensionKey] = []; } $this->dependencyErrors[$extensionKey][] = [ 'code' => $e->getCode(), 'message' => $e->getMessage() ]; } } } /** * Returns TRUE if a dependency error was found * * @return bool */ public function hasDependencyErrors() { return !empty($this->dependencyErrors); } /** * Return the dependency errors * * @return array */ public function getDependencyErrors() { return $this->dependencyErrors; } /** * Returns true if current TYPO3 version fulfills extension requirements * * @param Dependency $dependency * @throws Exception\UnresolvedTypo3DependencyException * @return bool */ protected function checkTypo3Dependency(Dependency $dependency) { $lowerCaseIdentifier = strtolower($dependency->getIdentifier()); if ($lowerCaseIdentifier === 'typo3') { if (!($dependency->getLowestVersion() === '') && version_compare(VersionNumberUtility::getNumericTypo3Version(), $dependency->getLowestVersion()) === -1) { throw new Exception\UnresolvedTypo3DependencyException( 'Your TYPO3 version is lower than this extension requires. It requires TYPO3 versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(), 1399144499 ); } if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), VersionNumberUtility::getNumericTypo3Version()) === -1) { throw new Exception\UnresolvedTypo3DependencyException( 'Your TYPO3 version is higher than this extension requires. It requires TYPO3 versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(), 1399144521 ); } } else { throw new Exception\UnresolvedTypo3DependencyException( 'checkTypo3Dependency can only check TYPO3 dependencies. Found dependency with identifier "' . $dependency->getIdentifier() . '"', 1399144551 ); } return true; } /** * Returns true if current php version fulfills extension requirements * * @param Dependency $dependency * @throws Exception\UnresolvedPhpDependencyException * @return bool */ protected function checkPhpDependency(Dependency $dependency) { $lowerCaseIdentifier = strtolower($dependency->getIdentifier()); if ($lowerCaseIdentifier === 'php') { if (!($dependency->getLowestVersion() === '') && version_compare(PHP_VERSION, $dependency->getLowestVersion()) === -1) { throw new Exception\UnresolvedPhpDependencyException( 'Your PHP version is lower than necessary. You need at least PHP version ' . $dependency->getLowestVersion(), 1377977857 ); } if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), PHP_VERSION) === -1) { throw new Exception\UnresolvedPhpDependencyException( 'Your PHP version is higher than allowed. You can use PHP versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(), 1377977856 ); } } else { throw new Exception\UnresolvedPhpDependencyException( 'checkPhpDependency can only check PHP dependencies. Found dependency with identifier "' . $dependency->getIdentifier() . '"', 1377977858 ); } return true; } /** * Main controlling function for checking dependencies * Dependency check is done in the following way: * - installed extension in matching version ? - return true * - available extension in matching version ? - mark for installation * - remote (TER) extension in matching version? - mark for download * * @todo handle exceptions / markForUpload * @param Dependency $dependency * @throws Exception\MissingVersionDependencyException * @return bool */ protected function checkExtensionDependency(Dependency $dependency) { $extensionKey = $dependency->getIdentifier(); $extensionIsLoaded = $this->isDependentExtensionLoaded($extensionKey); if ($extensionIsLoaded === true) { $isLoadedVersionCompatible = $this->isLoadedVersionCompatible($dependency); if ($isLoadedVersionCompatible === true || $this->skipDependencyCheck) { return true; } $extension = $this->listUtility->getExtension($extensionKey); $loadedVersion = $extension->getPackageMetaData()->getVersion(); if (version_compare($loadedVersion, $dependency->getHighestVersion()) === -1) { try { $this->getExtensionFromRepository($extensionKey, $dependency); } catch (Exception\UnresolvedDependencyException $e) { throw new Exception\MissingVersionDependencyException( 'The extension ' . $extensionKey . ' is installed in version ' . $loadedVersion . ' but needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion() . ' and could not be fetched from TER', 1396302624 ); } } else { throw new Exception\MissingVersionDependencyException( 'The extension ' . $extensionKey . ' is installed in version ' . $loadedVersion . ' but needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(), 1430561927 ); } } else { $extensionIsAvailable = $this->isDependentExtensionAvailable($extensionKey); if ($extensionIsAvailable === true) { $isAvailableVersionCompatible = $this->isAvailableVersionCompatible($dependency); if ($isAvailableVersionCompatible) { $unresolvedDependencyErrors = $this->dependencyErrors; $this->managementService->markExtensionForInstallation($extensionKey); $this->dependencyErrors = array_merge($unresolvedDependencyErrors, $this->dependencyErrors); } else { $extension = $this->listUtility->getExtension($extensionKey); $availableVersion = $extension->getPackageMetaData()->getVersion(); if (version_compare($availableVersion, $dependency->getHighestVersion()) === -1) { try { $this->getExtensionFromRepository($extensionKey, $dependency); } catch (Exception\MissingExtensionDependencyException $e) { if (!$this->skipDependencyCheck) { throw new Exception\MissingVersionDependencyException( 'The extension ' . $extensionKey . ' is available in version ' . $availableVersion . ' but is needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion() . ' and could not be fetched from TER', 1430560390 ); } } } else { if (!$this->skipDependencyCheck) { throw new Exception\MissingVersionDependencyException( 'The extension ' . $extensionKey . ' is available in version ' . $availableVersion . ' but is needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(), 1430562374 ); } // Dependency check is skipped and the local version has to be installed $this->managementService->markExtensionForInstallation($extensionKey); } } } else { $unresolvedDependencyErrors = $this->dependencyErrors; $this->getExtensionFromRepository($extensionKey, $dependency); $this->dependencyErrors = array_merge($unresolvedDependencyErrors, $this->dependencyErrors); } } return false; } /** * Get an extension from a repository * (might be in the extension itself or the TER) * * @param string $extensionKey * @param Dependency $dependency * @throws Exception\UnresolvedDependencyException */ protected function getExtensionFromRepository($extensionKey, Dependency $dependency) { if (!$this->getExtensionFromInExtensionRepository($extensionKey)) { $this->getExtensionFromTer($extensionKey, $dependency); } } /** * Gets an extension from the in extension repository * (the local extension storage) * * @param string $extensionKey * @return bool */ protected function getExtensionFromInExtensionRepository($extensionKey) { if ($this->localExtensionStorage !== '' && is_dir($this->localExtensionStorage)) { $extList = \TYPO3\CMS\Core\Utility\GeneralUtility::get_dirs($this->localExtensionStorage); if (in_array($extensionKey, $extList)) { $this->managementService->markExtensionForCopy($extensionKey, $this->localExtensionStorage); return true; } } return false; } /** * Handles checks to find a compatible extension version from TER to fulfill given dependency * * @todo unit tests * @param string $extensionKey * @param Dependency $dependency * @throws Exception\UnresolvedDependencyException */ protected function getExtensionFromTer($extensionKey, Dependency $dependency) { $isExtensionDownloadableFromTer = $this->isExtensionDownloadableFromTer($extensionKey); if (!$isExtensionDownloadableFromTer) { if (!$this->skipDependencyCheck) { if ($this->extensionRepository->countAll() > 0) { throw new Exception\MissingExtensionDependencyException( 'The extension ' . $extensionKey . ' is not available from TER.', 1399161266 ); } throw new Exception\MissingExtensionDependencyException( 'The extension ' . $extensionKey . ' could not be checked. Please update your Extension-List from TYPO3 Extension Repository (TER).', 1430580308 ); } return; } $isDownloadableVersionCompatible = $this->isDownloadableVersionCompatible($dependency); if (!$isDownloadableVersionCompatible) { if (!$this->skipDependencyCheck) { throw new Exception\MissingVersionDependencyException( 'No compatible version found for extension ' . $extensionKey, 1399161284 ); } return; } $latestCompatibleExtensionByIntegerVersionDependency = $this->getLatestCompatibleExtensionByIntegerVersionDependency($dependency); if (!$latestCompatibleExtensionByIntegerVersionDependency instanceof Extension) { if (!$this->skipDependencyCheck) { throw new Exception\MissingExtensionDependencyException( 'Could not resolve dependency for "' . $dependency->getIdentifier() . '"', 1399161302 ); } return; } if ($this->isDependentExtensionLoaded($extensionKey)) { $this->managementService->markExtensionForUpdate($latestCompatibleExtensionByIntegerVersionDependency); } else { $this->managementService->markExtensionForDownload($latestCompatibleExtensionByIntegerVersionDependency); } } /** * @param string $extensionKey * @return bool */ protected function isDependentExtensionLoaded($extensionKey) { return ExtensionManagementUtility::isLoaded($extensionKey); } /** * @param Dependency $dependency * @return bool */ protected function isLoadedVersionCompatible(Dependency $dependency) { $extensionVersion = ExtensionManagementUtility::getExtensionVersion($dependency->getIdentifier()); return $this->isVersionCompatible($extensionVersion, $dependency); } /** * @param string $version * @param Dependency $dependency * @return bool */ protected function isVersionCompatible($version, Dependency $dependency) { if (!($dependency->getLowestVersion() === '') && version_compare($version, $dependency->getLowestVersion()) === -1) { return false; } if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), $version) === -1) { return false; } return true; } /** * Checks whether the needed extension is available * (not necessarily installed, but present in system) * * @param string $extensionKey * @return bool */ protected function isDependentExtensionAvailable($extensionKey) { $this->setAvailableExtensions(); return array_key_exists($extensionKey, $this->availableExtensions); } /** * Checks whether the available version is compatible * * @param Dependency $dependency * @return bool */ protected function isAvailableVersionCompatible(Dependency $dependency) { $this->setAvailableExtensions(); $extensionData = $this->emConfUtility->includeEmConf($this->availableExtensions[$dependency->getIdentifier()]); return $this->isVersionCompatible($extensionData['version'], $dependency); } /** * Checks whether a ter extension with $extensionKey exists * * @param string $extensionKey * @return bool */ protected function isExtensionDownloadableFromTer($extensionKey) { return $this->extensionRepository->countByExtensionKey($extensionKey) > 0; } /** * Checks whether a compatible version of the extension exists in TER * * @param Dependency $dependency * @return bool */ protected function isDownloadableVersionCompatible(Dependency $dependency) { $versions = $this->getLowestAndHighestIntegerVersions($dependency); $count = $this->extensionRepository->countByVersionRangeAndExtensionKey( $dependency->getIdentifier(), $versions['lowestIntegerVersion'], $versions['highestIntegerVersion'] ); return !empty($count); } /** * Get the latest compatible version of an extension that's * compatible with the current core and PHP version. * * @param iterable $extensions * @return Extension|null */ protected function getCompatibleExtension(iterable $extensions): ?Extension { foreach ($extensions as $extension) { /** @var Extension $extension */ $this->checkDependencies($extension); $extensionKey = $extension->getExtensionKey(); if (isset($this->dependencyErrors[$extensionKey])) { // reset dependencyErrors and continue with next version unset($this->dependencyErrors[$extensionKey]); continue; } return $extension; } return null; } /** * Get the latest compatible version of an extension that * fulfills the given dependency from TER * * @param Dependency $dependency * @return Extension */ protected function getLatestCompatibleExtensionByIntegerVersionDependency(Dependency $dependency) { $versions = $this->getLowestAndHighestIntegerVersions($dependency); $compatibleDataSets = $this->extensionRepository->findByVersionRangeAndExtensionKeyOrderedByVersion( $dependency->getIdentifier(), $versions['lowestIntegerVersion'], $versions['highestIntegerVersion'] ); return $this->getCompatibleExtension($compatibleDataSets); } /** * Return array of lowest and highest version of dependency as integer * * @param Dependency $dependency * @return array */ protected function getLowestAndHighestIntegerVersions(Dependency $dependency) { $lowestVersion = $dependency->getLowestVersion(); $lowestVersionInteger = $lowestVersion ? VersionNumberUtility::convertVersionNumberToInteger($lowestVersion) : 0; $highestVersion = $dependency->getHighestVersion(); $highestVersionInteger = $highestVersion ? VersionNumberUtility::convertVersionNumberToInteger($highestVersion) : 0; return [ 'lowestIntegerVersion' => $lowestVersionInteger, 'highestIntegerVersion' => $highestVersionInteger ]; } /** * @param string $extensionKey * @return array */ public function findInstalledExtensionsThatDependOnMe($extensionKey) { $availableAndInstalledExtensions = $this->listUtility->getAvailableAndInstalledExtensionsWithAdditionalInformation(); $dependentExtensions = []; foreach ($availableAndInstalledExtensions as $availableAndInstalledExtensionKey => $availableAndInstalledExtension) { if (isset($availableAndInstalledExtension['installed']) && $availableAndInstalledExtension['installed'] === true) { if (is_array($availableAndInstalledExtension['constraints']) && is_array($availableAndInstalledExtension['constraints']['depends']) && array_key_exists($extensionKey, $availableAndInstalledExtension['constraints']['depends'])) { $dependentExtensions[] = $availableAndInstalledExtensionKey; } } } return $dependentExtensions; } /** * Get extensions (out of a given list) that are suitable for the current TYPO3 version * * @param \TYPO3\CMS\Extbase\Persistence\QueryResultInterface|array $extensions List of extensions to check * @return array List of extensions suitable for current TYPO3 version */ public function getExtensionsSuitableForTypo3Version($extensions) { $suitableExtensions = []; /** @var Extension $extension */ foreach ($extensions as $extension) { /** @var Dependency $dependency */ foreach ($extension->getDependencies() as $dependency) { if ($dependency->getIdentifier() === 'typo3') { try { if ($this->checkTypo3Dependency($dependency)) { $suitableExtensions[] = $extension; } } catch (Exception\UnresolvedTypo3DependencyException $e) { } break; } } } return $suitableExtensions; } /** * Gets a list of various extensions in various versions and returns * a filtered list containing the extension-version combination with * the highest version number. * * @param Extension[] $extensions * @param bool $showUnsuitable * * @return \TYPO3\CMS\Extensionmanager\Domain\Model\Extension[] */ public function filterYoungestVersionOfExtensionList(array $extensions, $showUnsuitable) { if (!$showUnsuitable) { $extensions = $this->getExtensionsSuitableForTypo3Version($extensions); } $filteredExtensions = []; foreach ($extensions as $extension) { $extensionKey = $extension->getExtensionKey(); if (!array_key_exists($extensionKey, $filteredExtensions)) { $filteredExtensions[$extensionKey] = $extension; continue; } $currentVersion = $filteredExtensions[$extensionKey]->getVersion(); $newVersion = $extension->getVersion(); if (version_compare($newVersion, $currentVersion, '>')) { $filteredExtensions[$extensionKey] = $extension; } } return $filteredExtensions; } }