459 lines
16 KiB
PHP
459 lines
16 KiB
PHP
<?php
|
|
namespace TYPO3\CMS\Extensionmanager\Service;
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Extbase\Object\ObjectManager;
|
|
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
|
|
use TYPO3\CMS\Extensionmanager\Domain\Model\Extension;
|
|
|
|
/**
|
|
* Service class for managing multiple step processes (dependencies for example)
|
|
*/
|
|
class ExtensionManagementService implements \TYPO3\CMS\Core\SingletonInterface
|
|
{
|
|
/**
|
|
* @var \TYPO3\CMS\Extensionmanager\Domain\Model\DownloadQueue
|
|
*/
|
|
protected $downloadQueue;
|
|
|
|
/**
|
|
* @var \TYPO3\CMS\Extensionmanager\Utility\DependencyUtility
|
|
*/
|
|
protected $dependencyUtility;
|
|
|
|
/**
|
|
* @var \TYPO3\CMS\Extensionmanager\Utility\InstallUtility
|
|
*/
|
|
protected $installUtility;
|
|
|
|
/**
|
|
* @var \TYPO3\CMS\Extensionmanager\Utility\ExtensionModelUtility
|
|
*/
|
|
protected $extensionModelUtility;
|
|
|
|
/**
|
|
* @var \TYPO3\CMS\Extensionmanager\Utility\DownloadUtility
|
|
*/
|
|
protected $downloadUtility;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected $automaticInstallationEnabled = true;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected $skipDependencyCheck = false;
|
|
|
|
/**
|
|
* @param \TYPO3\CMS\Extensionmanager\Domain\Model\DownloadQueue $downloadQueue
|
|
*/
|
|
public function injectDownloadQueue(\TYPO3\CMS\Extensionmanager\Domain\Model\DownloadQueue $downloadQueue)
|
|
{
|
|
$this->downloadQueue = $downloadQueue;
|
|
}
|
|
|
|
/**
|
|
* @param \TYPO3\CMS\Extensionmanager\Utility\DependencyUtility $dependencyUtility
|
|
*/
|
|
public function injectDependencyUtility(\TYPO3\CMS\Extensionmanager\Utility\DependencyUtility $dependencyUtility)
|
|
{
|
|
$this->dependencyUtility = $dependencyUtility;
|
|
}
|
|
|
|
/**
|
|
* @param \TYPO3\CMS\Extensionmanager\Utility\InstallUtility $installUtility
|
|
*/
|
|
public function injectInstallUtility(\TYPO3\CMS\Extensionmanager\Utility\InstallUtility $installUtility)
|
|
{
|
|
$this->installUtility = $installUtility;
|
|
}
|
|
|
|
/**
|
|
* @param \TYPO3\CMS\Extensionmanager\Utility\ExtensionModelUtility $extensionModelUtility
|
|
*/
|
|
public function injectExtensionModelUtility(\TYPO3\CMS\Extensionmanager\Utility\ExtensionModelUtility $extensionModelUtility)
|
|
{
|
|
$this->extensionModelUtility = $extensionModelUtility;
|
|
}
|
|
|
|
/**
|
|
* @param \TYPO3\CMS\Extensionmanager\Utility\DownloadUtility $downloadUtility
|
|
*/
|
|
public function injectDownloadUtility(\TYPO3\CMS\Extensionmanager\Utility\DownloadUtility $downloadUtility)
|
|
{
|
|
$this->downloadUtility = $downloadUtility;
|
|
}
|
|
|
|
/**
|
|
* @param string $extensionKey
|
|
*/
|
|
public function markExtensionForInstallation($extensionKey)
|
|
{
|
|
// We have to check for dependencies of the extension first, before marking it for installation
|
|
// because this extension might have dependencies, which need to be installed first
|
|
$this->installUtility->reloadAvailableExtensions();
|
|
$extension = $this->getExtension($extensionKey);
|
|
$this->dependencyUtility->checkDependencies($extension);
|
|
$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
|
|
*
|
|
* @param Extension $extension
|
|
*/
|
|
public function markExtensionForDownload(Extension $extension)
|
|
{
|
|
// We have to check for dependencies of the extension first, before marking it for download
|
|
// because this extension might have dependencies, which need to be downloaded and installed first
|
|
$this->dependencyUtility->checkDependencies($extension);
|
|
if (!$this->dependencyUtility->hasDependencyErrors()) {
|
|
$this->downloadQueue->addExtensionToQueue($extension);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Extension $extension
|
|
*/
|
|
public function markExtensionForUpdate(Extension $extension)
|
|
{
|
|
// We have to check for dependencies of the extension first, before marking it for download
|
|
// because this extension might have dependencies, which need to be downloaded and installed first
|
|
$this->dependencyUtility->checkDependencies($extension);
|
|
$this->downloadQueue->addExtensionToQueue($extension, 'update');
|
|
}
|
|
|
|
/**
|
|
* Enables or disables the dependency check for system environment (PHP, TYPO3) before extension installation
|
|
*
|
|
* @param bool $skipDependencyCheck
|
|
*/
|
|
public function setSkipDependencyCheck($skipDependencyCheck)
|
|
{
|
|
$this->skipDependencyCheck = $skipDependencyCheck;
|
|
}
|
|
|
|
/**
|
|
* @param bool $automaticInstallationEnabled
|
|
*/
|
|
public function setAutomaticInstallationEnabled($automaticInstallationEnabled)
|
|
{
|
|
$this->automaticInstallationEnabled = (bool)$automaticInstallationEnabled;
|
|
}
|
|
|
|
/**
|
|
* Install the extension
|
|
*
|
|
* @param Extension $extension
|
|
* @return bool|array Returns FALSE if dependencies cannot be resolved, otherwise array with installation information
|
|
*/
|
|
public function installExtension(Extension $extension)
|
|
{
|
|
$this->downloadExtension($extension);
|
|
if (!$this->checkDependencies($extension)) {
|
|
return false;
|
|
}
|
|
|
|
$downloadedDependencies = [];
|
|
$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']));
|
|
}
|
|
$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 = [];
|
|
if ($this->automaticInstallationEnabled) {
|
|
$installedDependencies = $this->installDependencies($installQueue);
|
|
}
|
|
|
|
return array_merge($downloadedDependencies, $updatedDependencies, $installedDependencies);
|
|
}
|
|
|
|
/**
|
|
* Returns the unresolved dependency errors
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getDependencyErrors()
|
|
{
|
|
return $this->dependencyUtility->getDependencyErrors();
|
|
}
|
|
|
|
/**
|
|
* @param string $extensionKey
|
|
* @return Extension
|
|
* @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
|
|
*/
|
|
public function getExtension($extensionKey)
|
|
{
|
|
return $this->extensionModelUtility->mapExtensionArrayToModel(
|
|
$this->installUtility->enrichExtensionWithDetails($extensionKey)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks if an extension is available in the system
|
|
*
|
|
* @param string $extensionKey
|
|
* @return bool
|
|
*/
|
|
public function isAvailable($extensionKey)
|
|
{
|
|
return $this->installUtility->isAvailable($extensionKey);
|
|
}
|
|
|
|
/**
|
|
* @param string $extensionKey
|
|
* @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageStateException if the package isn't available
|
|
* @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException if an invalid package key was passed
|
|
* @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException if an invalid package path was passed
|
|
* @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageManifestException if no extension configuration file could be found
|
|
*/
|
|
public function reloadPackageInformation($extensionKey)
|
|
{
|
|
$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
|
|
*
|
|
* @param Extension $extension
|
|
* @return bool Returns TRUE if all dependencies can be resolved, otherwise FALSE
|
|
*/
|
|
protected function checkDependencies(Extension $extension)
|
|
{
|
|
$this->dependencyUtility->setSkipDependencyCheck($this->skipDependencyCheck);
|
|
$this->dependencyUtility->checkDependencies($extension);
|
|
|
|
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->downloadUtility->getDownloadPath()];
|
|
$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
|
|
*
|
|
* @param Extension[] $updateQueue
|
|
* @return array
|
|
*/
|
|
protected function uninstallDependenciesToBeUpdated(array $updateQueue)
|
|
{
|
|
$resolvedDependencies = [];
|
|
foreach ($updateQueue as $extensionToUpdate) {
|
|
$this->installUtility->uninstall($extensionToUpdate->getExtensionKey());
|
|
$resolvedDependencies['updated'][$extensionToUpdate->getExtensionKey()] = $extensionToUpdate;
|
|
}
|
|
return $resolvedDependencies;
|
|
}
|
|
|
|
/**
|
|
* Install dependent extensions
|
|
*
|
|
* @param array $installQueue
|
|
* @return array
|
|
*/
|
|
protected function installDependencies(array $installQueue)
|
|
{
|
|
if (empty($installQueue)) {
|
|
return [];
|
|
}
|
|
$this->emitWillInstallExtensionsSignal($installQueue);
|
|
$resolvedDependencies = [];
|
|
$this->installUtility->install(...array_keys($installQueue));
|
|
foreach ($installQueue as $extensionKey => $_) {
|
|
$this->emitHasInstalledExtensionSignal($extensionKey);
|
|
if (!isset($resolvedDependencies['installed']) || !is_array($resolvedDependencies['installed'])) {
|
|
$resolvedDependencies['installed'] = [];
|
|
}
|
|
$resolvedDependencies['installed'][$extensionKey] = $extensionKey;
|
|
}
|
|
return $resolvedDependencies;
|
|
}
|
|
|
|
/**
|
|
* Download dependencies
|
|
* expects an array of extension objects to download
|
|
*
|
|
* @param Extension[] $downloadQueue
|
|
* @return array
|
|
*/
|
|
protected function downloadDependencies(array $downloadQueue)
|
|
{
|
|
$resolvedDependencies = [];
|
|
foreach ($downloadQueue as $extensionToDownload) {
|
|
$this->downloadUtility->download($extensionToDownload);
|
|
$this->downloadQueue->removeExtensionFromQueue($extensionToDownload);
|
|
$resolvedDependencies['downloaded'][$extensionToDownload->getExtensionKey()] = $extensionToDownload;
|
|
$this->markExtensionForInstallation($extensionToDownload->getExtensionKey());
|
|
}
|
|
return $resolvedDependencies;
|
|
}
|
|
|
|
/**
|
|
* Get and resolve dependencies
|
|
*
|
|
* @param Extension $extension
|
|
* @return array
|
|
*/
|
|
public function getAndResolveDependencies(Extension $extension)
|
|
{
|
|
$this->dependencyUtility->setSkipDependencyCheck($this->skipDependencyCheck);
|
|
$this->dependencyUtility->checkDependencies($extension);
|
|
$installQueue = $this->downloadQueue->getExtensionInstallStorage();
|
|
if (is_array($installQueue) && !empty($installQueue)) {
|
|
$installQueue = ['install' => $installQueue];
|
|
}
|
|
return array_merge($this->downloadQueue->getExtensionQueue(), $installQueue);
|
|
}
|
|
|
|
/**
|
|
* Downloads the extension the user wants to install
|
|
* This is separated from downloading the dependencies
|
|
* as an extension is able to provide it's own dependencies
|
|
*
|
|
* @param Extension $extension
|
|
*/
|
|
public function downloadMainExtension(Extension $extension)
|
|
{
|
|
// The extension object has a uid if the extension is not present in the system
|
|
// or an update of a present extension is triggered.
|
|
if ($extension->getUid()) {
|
|
$this->downloadUtility->download($extension);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array $installQueue
|
|
*/
|
|
protected function emitWillInstallExtensionsSignal(array $installQueue)
|
|
{
|
|
$this->getSignalSlotDispatcher()->dispatch(__CLASS__, 'willInstallExtensions', [$installQueue]);
|
|
}
|
|
|
|
/**
|
|
* @param string $extensionKey
|
|
* @deprecated since TYPO3 v9, will be removed with TYPO3v10
|
|
*/
|
|
protected function emitHasInstalledExtensionSignal($extensionKey)
|
|
{
|
|
$this->getSignalSlotDispatcher()->dispatch(__CLASS__, 'hasInstalledExtensions', [$extensionKey]);
|
|
}
|
|
|
|
/**
|
|
* Get the SignalSlot dispatcher
|
|
*
|
|
* @return Dispatcher
|
|
*/
|
|
protected function getSignalSlotDispatcher()
|
|
{
|
|
if (!isset($this->signalSlotDispatcher)) {
|
|
$this->signalSlotDispatcher = GeneralUtility::makeInstance(ObjectManager::class)
|
|
->get(Dispatcher::class);
|
|
}
|
|
return $this->signalSlotDispatcher;
|
|
}
|
|
}
|