[BUGFIX] Resolves extension dependencies recursively 28/50628/2
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:53:06 +0000 (12:53 +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.

Change-Id: I6ab77a9deb883ad9eb00bfcae33dd3133695328a
Resolves: #78666
Relates: #66152
Releases: master, 7.6
Reviewed-on: https://review.typo3.org/50628
Tested-by: TYPO3com <no-reply@typo3.com>
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 $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;
         }
 
             return false;
         }
 
-        $updatedDependencies = [];
-        $installedDependencies = [];
-        $queue = $this->downloadQueue->getExtensionQueue();
-        $copyQueue = $this->downloadQueue->getExtensionCopyStorage();
-
-        if (!empty($copyQueue)) {
-            $this->copyDependencies($copyQueue);
-        }
         $downloadedDependencies = [];
         $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);
     }
 
         return array_merge($downloadedDependencies, $updatedDependencies, $installedDependencies);
     }
 
index e2af810..ffe449a 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);
         $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
             ]
             '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);
         $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
             ]
             'update' => [
                 'foo' => $extensionModelMock
             ]