Skip to content
Snippets Groups Projects
Commit 0620f2d6 authored by Nicole Cordes's avatar Nicole Cordes Committed by Markus Klein
Browse files

[BUGFIX] Resolves extension dependencies recursively

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: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: default avatarHelmut Hummel <typo3@helhum.io>
Tested-by: default avatarHelmut Hummel <typo3@helhum.io>
Reviewed-by: default avatarMarkus Klein <markus.klein@typo3.org>
Tested-by: default avatarMarkus Klein <markus.klein@typo3.org>
parent 04bf0cbf
Branches
Tags
No related merge requests found
......@@ -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;
}
}
......@@ -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);
}
......
......@@ -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
]
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment