diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-92532-SupportForExtension-in-extensionInstallationInExtensionManagerRemoved.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-92532-SupportForExtension-in-extensionInstallationInExtensionManagerRemoved.rst new file mode 100644 index 0000000000000000000000000000000000000000..29056de877990cc32ca456bd84fd86135cf26be2 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-92532-SupportForExtension-in-extensionInstallationInExtensionManagerRemoved.rst @@ -0,0 +1,46 @@ +.. include:: ../../Includes.txt + +=============================================================================================== +Breaking: #12345 - Support for extension-in-extension installation in Extension Manager removed +=============================================================================================== + +See :issue:`92532` + +Description +=========== + +The installation process within the Extension Manager allowed extensions to be +installed having custom dependencies to other extensions in +`EXT:my_extension/Initialisation/Extensions/third_party_ext`. + +This feature was originally introduced for the Introduction Package, +which had a few more dependencies until TYPO3 v9. + +As this (undocumented) feature was not used in public for any other extensions, +and since Extension Manager can fetch dependencies from TER directly as well, +this feature is removed. + + +Impact +====== + +If an extension is installed which contains other extensions as +dependencies in `Initialisation/Extensions/*` they are now ignored +on installation, and instead looked up in the remote TYPO3 Extension Repository, +as with any other depending extension. + + +Affected Installations +====================== + +TYPO3 extensions using this dependency management as "Extension-in-Extension" +functionality. + + +Migration +========= + +Upload the proper extension into https://extensions.typo3.org and remove +the folder `Initialisation/Extensions` from any custom extensions. + +.. index:: PHP-API, FullyScanned, ext:extensionmanager diff --git a/typo3/sysext/extensionmanager/Classes/Domain/Model/DownloadQueue.php b/typo3/sysext/extensionmanager/Classes/Domain/Model/DownloadQueue.php index 5c312d9995f842622f2f01aed6480a9137a19b1d..3bdfec460eb32440c24da05f624bb02c530bd7f7 100644 --- a/typo3/sysext/extensionmanager/Classes/Domain/Model/DownloadQueue.php +++ b/typo3/sysext/extensionmanager/Classes/Domain/Model/DownloadQueue.php @@ -38,13 +38,6 @@ class DownloadQueue implements SingletonInterface */ protected $extensionInstallStorage = []; - /** - * Storage for extensions to be copied - * - * @var array - */ - protected $extensionCopyStorage = []; - /** * Adds an extension to the download queue. * If the extension was already requested in a different version @@ -111,29 +104,6 @@ class DownloadQueue implements SingletonInterface $this->extensionInstallStorage[$extension->getExtensionKey()] = $extension; } - /** - * Adds an extension to the copy queue for later copying - * - * @param string $extensionKey - * @param string $sourceFolder - */ - public function addExtensionToCopyQueue($extensionKey, $sourceFolder) - { - $this->extensionCopyStorage[$extensionKey] = $sourceFolder; - } - - /** - * Remove an extension from extension copy storage - * - * @param string $extensionKey - */ - public function removeExtensionFromCopyQueue($extensionKey) - { - if (array_key_exists($extensionKey, $this->extensionCopyStorage)) { - unset($this->extensionCopyStorage[$extensionKey]); - } - } - /** * Gets the extension installation queue * @@ -155,16 +125,6 @@ class DownloadQueue implements SingletonInterface return empty($this->extensionStorage[$stack]); } - /** - * Return whether the copy queue contains extensions or not - * - * @return bool - */ - public function isCopyQueueEmpty() - { - return empty($this->extensionCopyStorage); - } - /** * Resets the extension queue and returns old extensions * @@ -185,18 +145,6 @@ class DownloadQueue implements SingletonInterface return $storage; } - /** - * Resets the copy queue and returns the old extensions - * @return array - */ - public function resetExtensionCopyStorage() - { - $storage = $this->extensionCopyStorage; - $this->extensionCopyStorage = []; - - return $storage; - } - /** * Resets the install queue and returns the old extensions * @return array diff --git a/typo3/sysext/extensionmanager/Classes/Service/ExtensionManagementService.php b/typo3/sysext/extensionmanager/Classes/Service/ExtensionManagementService.php index de146080e78049c74298fc0932109abe37e3e31d..2fc316b9087b443f2ba95aea076db49416163ba7 100644 --- a/typo3/sysext/extensionmanager/Classes/Service/ExtensionManagementService.php +++ b/typo3/sysext/extensionmanager/Classes/Service/ExtensionManagementService.php @@ -141,17 +141,6 @@ class ExtensionManagementService implements SingletonInterface $this->downloadQueue->addExtensionToInstallQueue($extension); } - /** - * Mark an extension for copy - * - * @param string $extensionKey - * @param string $sourceFolder - */ - public function markExtensionForCopy($extensionKey, $sourceFolder) - { - $this->downloadQueue->addExtensionToCopyQueue($extensionKey, $sourceFolder); - } - /** * Mark an extension for download * @@ -204,7 +193,7 @@ class ExtensionManagementService implements SingletonInterface */ public function installExtension(Extension $extension) { - $this->downloadExtension($extension); + $this->downloadMainExtension($extension); if (!$this->checkDependencies($extension)) { return false; } @@ -217,16 +206,9 @@ class ExtensionManagementService implements SingletonInterface // added each time // Extensions have to be installed in reverse order. Extensions which were added at last are dependencies of // earlier ones and need to be available before - while (!$this->downloadQueue->isCopyQueueEmpty() - || !$this->downloadQueue->isQueueEmpty('download') + while (!$this->downloadQueue->isQueueEmpty('download') || !$this->downloadQueue->isQueueEmpty('update') ) { - // First copy all available extension - // This might change other queues again - $copyQueue = $this->downloadQueue->resetExtensionCopyStorage(); - if (!empty($copyQueue)) { - $this->copyDependencies($copyQueue); - } $installQueue = array_merge($this->downloadQueue->resetExtensionInstallStorage(), $installQueue); // Get download and update information $queue = $this->downloadQueue->resetExtensionQueue(); @@ -304,17 +286,6 @@ class ExtensionManagementService implements SingletonInterface $this->installUtility->reloadPackageInformation($extensionKey); } - /** - * Download an extension - * - * @param Extension $extension - */ - protected function downloadExtension(Extension $extension) - { - $this->downloadMainExtension($extension); - $this->setInExtensionRepository($extension->getExtensionKey()); - } - /** * Check dependencies for an extension and its required extensions * @@ -329,41 +300,6 @@ class ExtensionManagementService implements SingletonInterface return !$this->dependencyUtility->hasDependencyErrors(); } - /** - * Sets the path to the repository in an extension - * (Initialisation/Extensions) depending on the extension - * that is currently installed - * - * @param string $extensionKey - */ - protected function setInExtensionRepository($extensionKey) - { - $paths = Extension::returnInstallPaths(); - $path = $paths[$this->downloadPath] ?? ''; - if (empty($path)) { - return; - } - $localExtensionStorage = $path . $extensionKey . '/Initialisation/Extensions/'; - $this->dependencyUtility->setLocalExtensionStorage($localExtensionStorage); - } - - /** - * Copies locally provided extensions to typo3conf/ext - * - * @param array $copyQueue - */ - protected function copyDependencies(array $copyQueue) - { - $installPaths = Extension::returnAllowedInstallPaths(); - foreach ($copyQueue as $extensionKey => $sourceFolder) { - $destination = $installPaths['Local'] . $extensionKey; - GeneralUtility::mkdir($destination); - GeneralUtility::copyDirectory($sourceFolder . $extensionKey, $destination); - $this->markExtensionForInstallation($extensionKey); - $this->downloadQueue->removeExtensionFromCopyQueue($extensionKey); - } - } - /** * Uninstall extensions that will be updated * This is not strictly necessary but cleaner all in all diff --git a/typo3/sysext/extensionmanager/Classes/Utility/DependencyUtility.php b/typo3/sysext/extensionmanager/Classes/Utility/DependencyUtility.php index 971be9fc9179a12901cb91a4802274560f1273f6..6cf26175dcfe7c4bfc8674ddd255654bde46893c 100644 --- a/typo3/sysext/extensionmanager/Classes/Utility/DependencyUtility.php +++ b/typo3/sysext/extensionmanager/Classes/Utility/DependencyUtility.php @@ -17,7 +17,6 @@ namespace TYPO3\CMS\Extensionmanager\Utility; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; -use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\VersionNumberUtility; use TYPO3\CMS\Extensionmanager\Domain\Model\Dependency; use TYPO3\CMS\Extensionmanager\Domain\Model\Extension; @@ -61,11 +60,6 @@ class DependencyUtility implements SingletonInterface */ protected $availableExtensions = []; - /** - * @var string - */ - protected $localExtensionStorage = ''; - /** * @var array */ @@ -108,14 +102,6 @@ class DependencyUtility implements SingletonInterface $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 @@ -282,7 +268,7 @@ class DependencyUtility implements SingletonInterface $loadedVersion = $extension->getPackageMetaData()->getVersion(); if (version_compare($loadedVersion, $dependency->getHighestVersion()) === -1) { try { - $this->getExtensionFromRepository($extensionKey, $dependency); + $this->downloadExtensionFromRemote($extensionKey, $dependency); } catch (UnresolvedDependencyException $e) { throw new MissingVersionDependencyException( 'The extension ' . $extensionKey . ' is installed in version ' . $loadedVersion @@ -310,7 +296,7 @@ class DependencyUtility implements SingletonInterface $availableVersion = $extension->getPackageMetaData()->getVersion(); if (version_compare($availableVersion, $dependency->getHighestVersion()) === -1) { try { - $this->getExtensionFromRepository($extensionKey, $dependency); + $this->downloadExtensionFromRemote($extensionKey, $dependency); } catch (MissingExtensionDependencyException $e) { if (!$this->skipDependencyCheck) { throw new MissingVersionDependencyException( @@ -334,7 +320,7 @@ class DependencyUtility implements SingletonInterface } } else { $unresolvedDependencyErrors = $this->dependencyErrors; - $this->getExtensionFromRepository($extensionKey, $dependency); + $this->downloadExtensionFromRemote($extensionKey, $dependency); $this->dependencyErrors = array_merge($unresolvedDependencyErrors, $this->dependencyErrors); } } @@ -342,53 +328,16 @@ class DependencyUtility implements SingletonInterface 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 = GeneralUtility::get_dirs($this->localExtensionStorage); - $extList = is_array($extList) ? $extList : []; - 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) + protected function downloadExtensionFromRemote(string $extensionKey, Dependency $dependency) { - $isExtensionDownloadableFromTer = $this->isExtensionDownloadableFromTer($extensionKey); - if (!$isExtensionDownloadableFromTer) { + if (!$this->isExtensionDownloadableFromRemote($extensionKey)) { if (!$this->skipDependencyCheck) { if ($this->extensionRepository->countAll() > 0) { throw new MissingExtensionDependencyException( @@ -503,7 +452,7 @@ class DependencyUtility implements SingletonInterface * @param string $extensionKey * @return bool */ - protected function isExtensionDownloadableFromTer($extensionKey) + protected function isExtensionDownloadableFromRemote($extensionKey) { return $this->extensionRepository->countByExtensionKey($extensionKey) > 0; } diff --git a/typo3/sysext/extensionmanager/Tests/Unit/Service/ExtensionManagementServiceTest.php b/typo3/sysext/extensionmanager/Tests/Unit/Service/ExtensionManagementServiceTest.php index b41b338da52f3b2d2c3a9498715f04ea732ac2ae..d2b537cf62ac90cbb30463b30f750f74eadadd07 100644 --- a/typo3/sysext/extensionmanager/Tests/Unit/Service/ExtensionManagementServiceTest.php +++ b/typo3/sysext/extensionmanager/Tests/Unit/Service/ExtensionManagementServiceTest.php @@ -100,7 +100,6 @@ class ExtensionManagementServiceTest extends UnitTestCase public function installExtensionReturnsFalseIfDependenciesCannotBeResolved(): void { $extension = new Extension(); - $this->dependencyUtilityProphecy->setLocalExtensionStorage(Argument::any())->willReturn(); $this->dependencyUtilityProphecy->setSkipDependencyCheck(false)->willReturn(); $this->dependencyUtilityProphecy->checkDependencies($extension)->willReturn(); @@ -166,16 +165,6 @@ class ExtensionManagementServiceTest extends UnitTestCase self::assertSame(['updated' => ['foo' => $extension], 'installed' => ['foo' => 'foo']], $result); } - /** - * @test - */ - public function markExtensionForCopyAddsExtensionToCopyQueue(): void - { - $this->managementService->markExtensionForCopy('ext', 'some/folder/'); - - self::assertSame(['ext' => 'some/folder/'], $this->downloadQueue->resetExtensionCopyStorage()); - } - /** * @test */ diff --git a/typo3/sysext/extensionmanager/Tests/Unit/Utility/DependencyUtilityTest.php b/typo3/sysext/extensionmanager/Tests/Unit/Utility/DependencyUtilityTest.php index 1de583dedcbd994851f8523ced1ea19bacb6c324..a7998cf8ac63975e879381ad457c254473608016 100644 --- a/typo3/sysext/extensionmanager/Tests/Unit/Utility/DependencyUtilityTest.php +++ b/typo3/sysext/extensionmanager/Tests/Unit/Utility/DependencyUtilityTest.php @@ -380,7 +380,7 @@ class DependencyUtilityTest extends UnitTestCase /** * @test */ - public function isExtensionDownloadableFromTerReturnsTrueIfOneVersionExists(): void + public function isExtensionDownloadableFromRemoteReturnsTrueIfOneVersionExists(): void { $extensionRepositoryMock = $this->getMockBuilder(ExtensionRepository::class) ->setMethods(['countByExtensionKey']) @@ -389,7 +389,7 @@ class DependencyUtilityTest extends UnitTestCase $extensionRepositoryMock->expects(self::once())->method('countByExtensionKey')->with('test123')->willReturn(1); $dependencyUtility = $this->getAccessibleMock(DependencyUtility::class, ['dummy']); $dependencyUtility->_set('extensionRepository', $extensionRepositoryMock); - $count = $dependencyUtility->_call('isExtensionDownloadableFromTer', 'test123'); + $count = $dependencyUtility->_call('isExtensionDownloadableFromRemote', 'test123'); self::assertTrue($count); } @@ -397,7 +397,7 @@ class DependencyUtilityTest extends UnitTestCase /** * @test */ - public function isExtensionDownloadableFromTerReturnsFalseIfNoVersionExists() + public function isExtensionDownloadableFromRemoteReturnsFalseIfNoVersionExists() { $extensionRepositoryMock = $this->getMockBuilder(ExtensionRepository::class) ->setMethods(['countByExtensionKey']) @@ -406,7 +406,7 @@ class DependencyUtilityTest extends UnitTestCase $extensionRepositoryMock->expects(self::once())->method('countByExtensionKey')->with('test123')->willReturn(0); $dependencyUtility = $this->getAccessibleMock(DependencyUtility::class, ['dummy']); $dependencyUtility->_set('extensionRepository', $extensionRepositoryMock); - $count = $dependencyUtility->_call('isExtensionDownloadableFromTer', 'test123'); + $count = $dependencyUtility->_call('isExtensionDownloadableFromRemote', 'test123'); self::assertFalse($count); }