From 098ccde77a316b617bbf9f83424c01339ab20a10 Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Fri, 9 Oct 2020 21:45:04 +0200
Subject: [PATCH] [!!!][TASK] EM: Drop support for extensions-in-extensions
 installation

A feature which was built for the introduction package back then, which
allowed to ship a modified / custom extension as dependency, where
one could put another extension in EXT:my_ext/Initialisation/Extensions/another_ext
is removed, as this special case is

a) not needed anymore
b) not following any rules of being conformant for "dependency hell"
c) specific for dependency handling of TYPO3 (non-composer)

Resolves: #92532
Releases: master
Change-Id: I7e7d8681a81e184835122ae221971e5eab435778
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/66097
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Daniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Daniel Goerz <daniel.goerz@posteo.de>
---
 ...nInstallationInExtensionManagerRemoved.rst | 46 +++++++++++++
 .../Classes/Domain/Model/DownloadQueue.php    | 52 --------------
 .../Service/ExtensionManagementService.php    | 68 +------------------
 .../Classes/Utility/DependencyUtility.php     | 63 ++---------------
 .../ExtensionManagementServiceTest.php        | 11 ---
 .../Unit/Utility/DependencyUtilityTest.php    |  8 +--
 6 files changed, 58 insertions(+), 190 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Breaking-92532-SupportForExtension-in-extensionInstallationInExtensionManagerRemoved.rst

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 000000000000..29056de87799
--- /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 5c312d9995f8..3bdfec460eb3 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 de146080e780..2fc316b9087b 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 971be9fc9179..6cf26175dcfe 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 b41b338da52f..d2b537cf62ac 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 1de583dedcbd..a7998cf8ac63 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);
     }
-- 
GitLab