[BUGFIX] Resolves extension dependencies recursively 02/50602/7
authorNicole Cordes <typo3@cordes.co>
Sat, 12 Nov 2016 09:19:10 +0000 (10:19 +0100)
committerMarkus Klein <markus.klein@typo3.org>
Sun, 13 Nov 2016 11:37:04 +0000 (12:37 +0100)
Currently it is not possible to install an extension which has special
sub-dependencies (e.g. dependencies of sub-extensions). During the
installation of an extension, the download information is fetched too
early and might not contain all necessary downloads. Furthermore later
installation actions add new dependencies, which have to be resolved
before any other extension can be installed.

This patch ensures all dependencies and their sub-dependencies are
fetched before the first installation. All installation information is
now correctly ordered, as the last one added has to be the first one in
the installation queue.

Resolves: #78666
Relates: #66152
Releases: master, 7.6
Change-Id: Idd9242aa1e2ecac3deb542290627fdf9c5479edc
Reviewed-on: https://review.typo3.org/50602
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Helmut Hummel <typo3@helhum.io>
Tested-by: Helmut Hummel <typo3@helhum.io>
Reviewed-by: Markus Klein <markus.klein@typo3.org>
Tested-by: Markus Klein <markus.klein@typo3.org>
typo3/sysext/extensionmanager/Classes/Domain/Model/DownloadQueue.php
typo3/sysext/extensionmanager/Classes/Service/ExtensionManagementService.php
typo3/sysext/extensionmanager/Tests/Unit/Service/ExtensionManagementServiceTest.php

index 6a96394..cb09124 100644 (file)
@@ -181,4 +181,79 @@ class DownloadQueue implements \TYPO3\CMS\Core\SingletonInterface
     {
         return $this->extensionCopyStorage;
     }
+
+    /**
+     * Return whether the queue contains extensions or not
+     *
+     * @param string $stack
+     * @return bool
+     */
+    public function isQueueEmpty($stack = 'download')
+    {
+        return empty($this->extensionStorage[$stack]);
+    }
+
+    /**
+     * Return whether the copy queue contains extensions or not
+     *
+     * @return bool
+     */
+    public function isCopyQueueEmpty()
+    {
+        return empty($this->extensionCopyStorage);
+    }
+
+    /**
+     * Return whether the install queue contains extensions or not
+     *
+     * @return bool
+     */
+    public function isInstallQueueEmpty()
+    {
+        return empty($this->extensionInstallStorage);
+    }
+
+    /**
+     * Resets the extension queue and returns old extensions
+     *
+     * @param string|null $stack if null, all stacks are reset
+     * @return array
+     */
+    public function resetExtensionQueue($stack = null)
+    {
+        $storage = [];
+        if ($stack === null) {
+            $storage = $this->extensionStorage;
+            $this->extensionStorage = [];
+        } elseif (isset($this->extensionStorage[$stack])) {
+            $storage = $this->extensionStorage[$stack];
+            $this->extensionStorage[$stack] = [];
+        }
+
+        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
+     */
+    public function resetExtensionInstallStorage()
+    {
+        $storage = $this->extensionInstallStorage;
+        $this->extensionInstallStorage = [];
+
+        return $storage;
+    }
 }
index 149276b..b3b6dca 100644 (file)
@@ -184,30 +184,50 @@ class ExtensionManagementService implements \TYPO3\CMS\Core\SingletonInterface
             return false;
         }
 
-        $updatedDependencies = [];
-        $installedDependencies = [];
-        $queue = $this->downloadQueue->getExtensionQueue();
-        $copyQueue = $this->downloadQueue->getExtensionCopyStorage();
-
-        if (!empty($copyQueue)) {
-            $this->copyDependencies($copyQueue);
-        }
         $downloadedDependencies = [];
-        if (array_key_exists('download', $queue)) {
-            $downloadedDependencies = $this->downloadDependencies($queue['download']);
-        }
-        if ($this->automaticInstallationEnabled) {
-            if (array_key_exists('update', $queue)) {
-                $this->downloadDependencies($queue['update']);
-                $updatedDependencies = $this->uninstallDependenciesToBeUpdated($queue['update']);
+        $updatedDependencies = [];
+        $installQueue = [];
+
+        // First resolve all dependencies and the sub-dependencies until all queues are empty as new extensions might be
+        // 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')
+            || !$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();
+            if (!empty($queue['download'])) {
+                $downloadedDependencies = array_merge($downloadedDependencies, $this->downloadDependencies($queue['download']));
             }
-            // add extension at the end of the download queue
-            $this->downloadQueue->addExtensionToInstallQueue($extension);
-            $installQueue = $this->downloadQueue->getExtensionInstallStorage();
-            if (!empty($installQueue)) {
-                $installedDependencies = $this->installDependencies($installQueue);
+            $installQueue = array_merge($this->downloadQueue->resetExtensionInstallStorage(), $installQueue);
+            if ($this->automaticInstallationEnabled) {
+                if (!empty($queue['update'])) {
+                    $this->downloadDependencies($queue['update']);
+                    $updatedDependencies = array_merge($updatedDependencies, $this->uninstallDependenciesToBeUpdated($queue['update']));
+                }
+                $installQueue = array_merge($this->downloadQueue->resetExtensionInstallStorage(), $installQueue);
             }
         }
+
+        // If there were any dependency errors we have to abort here
+        if ($this->dependencyUtility->hasDependencyErrors()) {
+            return false;
+        }
+
+        // Attach extension to install queue
+        $this->downloadQueue->addExtensionToInstallQueue($extension);
+        $installQueue += $this->downloadQueue->resetExtensionInstallStorage();
+        $installedDependencies = $this->installDependencies($installQueue);
+
         return array_merge($downloadedDependencies, $updatedDependencies, $installedDependencies);
     }
 
index eff4fe9..caf3105 100644 (file)
@@ -38,8 +38,12 @@ class ExtensionManagementServiceTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $dependencyUtilityMock = $this->getAccessibleMock(\TYPO3\CMS\Extensionmanager\Utility\DependencyUtility::class, ['checkDependencies']);
         $dependencyUtilityMock->expects($this->atLeastOnce())->method('checkDependencies');
         $managementMock->_set('dependencyUtility', $dependencyUtilityMock);
-        $downloadQueueMock = $this->getAccessibleMock(\TYPO3\CMS\Extensionmanager\Domain\Model\DownloadQueue::class, ['getExtensionQueue', 'addExtensionToInstallQueue']);
-        $downloadQueueMock->expects($this->atLeastOnce())->method('getExtensionQueue')->will($this->returnValue([
+        $downloadQueueMock = $this->getAccessibleMock(\TYPO3\CMS\Extensionmanager\Domain\Model\DownloadQueue::class, ['isCopyQueueEmpty', 'isQueueEmpty', 'resetExtensionQueue', 'addExtensionToInstallQueue']);
+        $downloadQueueMock->expects($this->any())->method('isCopyQueueEmpty')->willReturn(true);
+        $downloadQueueMock->expects($this->at(1))->method('isQueueEmpty')->with('download')->willReturn(false);
+        $downloadQueueMock->expects($this->at(4))->method('isQueueEmpty')->with('download')->willReturn(true);
+        $downloadQueueMock->expects($this->at(5))->method('isQueueEmpty')->with('update')->willReturn(true);
+        $downloadQueueMock->expects($this->atLeastOnce())->method('resetExtensionQueue')->will($this->returnValue([
             'download' => [
                 'foo' => $extensionModelMock
             ]
@@ -67,8 +71,12 @@ class ExtensionManagementServiceTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $dependencyUtilityMock = $this->getAccessibleMock(\TYPO3\CMS\Extensionmanager\Utility\DependencyUtility::class, ['checkDependencies']);
         $dependencyUtilityMock->expects($this->atLeastOnce())->method('checkDependencies');
         $managementMock->_set('dependencyUtility', $dependencyUtilityMock);
-        $downloadQueueMock = $this->getAccessibleMock(\TYPO3\CMS\Extensionmanager\Domain\Model\DownloadQueue::class, ['getExtensionQueue', 'addExtensionToInstallQueue']);
-        $downloadQueueMock->expects($this->atLeastOnce())->method('getExtensionQueue')->will($this->returnValue([
+        $downloadQueueMock = $this->getAccessibleMock(\TYPO3\CMS\Extensionmanager\Domain\Model\DownloadQueue::class, ['isCopyQueueEmpty', 'isQueueEmpty', 'resetExtensionQueue', 'addExtensionToInstallQueue']);
+        $downloadQueueMock->expects($this->any())->method('isCopyQueueEmpty')->willReturn(true);
+        $downloadQueueMock->expects($this->at(1))->method('isQueueEmpty')->with('download')->willReturn(false);
+        $downloadQueueMock->expects($this->at(4))->method('isQueueEmpty')->with('download')->willReturn(true);
+        $downloadQueueMock->expects($this->at(5))->method('isQueueEmpty')->with('update')->willReturn(true);
+        $downloadQueueMock->expects($this->atLeastOnce())->method('resetExtensionQueue')->will($this->returnValue([
             'update' => [
                 'foo' => $extensionModelMock
             ]