| Current Path : /var/www/surf/TYPO3/vendor/typo3/cms-extensionmanager/Classes/Utility/ |
| Current File : /var/www/surf/TYPO3/vendor/typo3/cms-extensionmanager/Classes/Utility/InstallUtility.php |
<?php
/*
* 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!
*/
namespace TYPO3\CMS\Extensionmanager\Utility;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Finder\Finder;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Configuration\Exception\SiteConfigurationWriteException;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Configuration\SiteConfiguration;
use TYPO3\CMS\Core\Core\BootService;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
use TYPO3\CMS\Core\Database\Schema\SqlReader;
use TYPO3\CMS\Core\Package\Event\AfterPackageActivationEvent;
use TYPO3\CMS\Core\Package\Event\AfterPackageDeactivationEvent;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Service\OpcodeCacheService;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use TYPO3\CMS\Extensionmanager\Domain\Model\Extension;
use TYPO3\CMS\Extensionmanager\Event\AfterExtensionDatabaseContentHasBeenImportedEvent;
use TYPO3\CMS\Extensionmanager\Event\AfterExtensionFilesHaveBeenImportedEvent;
use TYPO3\CMS\Extensionmanager\Event\AfterExtensionSiteFilesHaveBeenImportedEvent;
use TYPO3\CMS\Extensionmanager\Event\AfterExtensionStaticDatabaseContentHasBeenImportedEvent;
use TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException;
use TYPO3\CMS\Impexp\Import;
use TYPO3\CMS\Impexp\Utility\ImportExportUtility;
/**
* Extension Manager Install Utility
* @internal This class is a specific ExtensionManager implementation and is not part of the Public TYPO3 API.
*/
class InstallUtility implements SingletonInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* @var \TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility
*/
protected $fileHandlingUtility;
/**
* @var \TYPO3\CMS\Extensionmanager\Utility\ListUtility
*/
protected $listUtility;
/**
* @var \TYPO3\CMS\Core\Package\PackageManager
*/
protected $packageManager;
/**
* @var \TYPO3\CMS\Core\Cache\CacheManager
*/
protected $cacheManager;
/**
* @var \TYPO3\CMS\Core\Registry
*/
protected $registry;
/**
* @var EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* @var BootService
*/
protected $bootService;
public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
public function injectFileHandlingUtility(FileHandlingUtility $fileHandlingUtility)
{
$this->fileHandlingUtility = $fileHandlingUtility;
}
public function injectListUtility(ListUtility $listUtility)
{
$this->listUtility = $listUtility;
}
public function injectPackageManager(PackageManager $packageManager)
{
$this->packageManager = $packageManager;
}
public function injectCacheManager(CacheManager $cacheManager)
{
$this->cacheManager = $cacheManager;
}
public function injectRegistry(Registry $registry)
{
$this->registry = $registry;
}
public function injectBootService(BootService $bootService)
{
$this->bootService = $bootService;
}
/**
* Helper function to install an extension and processes db updates.
*
* @param string ...$extensionKeys
* @throws ExtensionManagerException
*/
public function install(...$extensionKeys)
{
foreach ($extensionKeys as $extensionKey) {
$this->loadExtension($extensionKey);
$extension = $this->enrichExtensionWithDetails($extensionKey, false);
$this->saveDefaultConfiguration($extensionKey);
}
$this->cacheManager->flushCaches();
// Load a new container as reloadCaches will load ext_localconf
$container = $this->bootService->getContainer(false);
$backup = $this->bootService->makeCurrent($container);
$this->reloadCaches();
$this->updateDatabase();
foreach ($extensionKeys as $extensionKey) {
$this->processExtensionSetup($extensionKey);
$container->get(EventDispatcherInterface::class)->dispatch(new AfterPackageActivationEvent($extensionKey, 'typo3-cms-extension', $this));
}
// Reset to the original container instance
$this->bootService->makeCurrent(null, $backup);
}
public function processExtensionSetup(string $extensionKey): void
{
$packagePath = $this->packageManager->getPackage($extensionKey)->getPackagePath();
$this->importInitialFiles($packagePath, $extensionKey);
$this->importStaticSqlFile($extensionKey, $packagePath);
$import = $this->importT3DFile($extensionKey, $packagePath);
$this->importSiteConfiguration($extensionKey, $packagePath, $import);
}
/**
* Helper function to uninstall an extension
*
* @param string $extensionKey
* @throws ExtensionManagerException
*/
public function uninstall($extensionKey)
{
$dependentExtensions = $this->findInstalledExtensionsThatDependOnExtension((string)$extensionKey);
if (!empty($dependentExtensions)) {
throw new ExtensionManagerException(
LocalizationUtility::translate(
'extensionList.uninstall.dependencyError',
'extensionmanager',
[$extensionKey, implode(',', $dependentExtensions)]
) ?? '',
1342554622
);
}
$this->unloadExtension($extensionKey);
}
/**
* Find installed extensions which depend on the given extension.
* This is used at extension uninstall to stop the process if an installed
* extension depends on the extension to be uninstalled.
*/
protected function findInstalledExtensionsThatDependOnExtension(string $extensionKey): array
{
$availableAndInstalledExtensions = $this->listUtility->getAvailableAndInstalledExtensionsWithAdditionalInformation();
$dependentExtensions = [];
foreach ($availableAndInstalledExtensions as $availableAndInstalledExtensionKey => $availableAndInstalledExtension) {
if (isset($availableAndInstalledExtension['installed']) && $availableAndInstalledExtension['installed'] === true) {
if (is_array($availableAndInstalledExtension['constraints'] ?? false) && is_array($availableAndInstalledExtension['constraints']['depends']) && array_key_exists($extensionKey, $availableAndInstalledExtension['constraints']['depends'])) {
$dependentExtensions[] = $availableAndInstalledExtensionKey;
}
}
}
return $dependentExtensions;
}
/**
* Reset and reload the available extensions
*/
public function reloadAvailableExtensions()
{
$this->listUtility->reloadAvailableExtensions();
}
/**
* Wrapper function for loading extensions
*
* @param string $extensionKey
*/
protected function loadExtension($extensionKey)
{
$this->packageManager->activatePackage($extensionKey);
}
/**
* Wrapper function for unloading extensions
*
* @param string $extensionKey
*/
protected function unloadExtension($extensionKey)
{
$this->packageManager->deactivatePackage($extensionKey);
$this->eventDispatcher->dispatch(new AfterPackageDeactivationEvent($extensionKey, 'typo3-cms-extension', $this));
$this->cacheManager->flushCachesInGroup('system');
}
/**
* Checks if an extension is available in the system
*
* @param string $extensionKey
* @return bool
*/
public function isAvailable($extensionKey)
{
return $this->packageManager->isPackageAvailable($extensionKey);
}
/**
* Reloads the package information, if the package is already registered
*
* @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)
{
if ($this->packageManager->isPackageAvailable($extensionKey)) {
$this->reloadOpcache();
$this->packageManager->reloadPackageInformation($extensionKey);
}
}
/**
* Fetch additional information for an extension key
*
* @param string $extensionKey
* @param bool $loadTerInformation
* @return array
* @throws ExtensionManagerException
* @internal
*/
public function enrichExtensionWithDetails($extensionKey, $loadTerInformation = true)
{
$extension = $this->getExtensionArray($extensionKey);
if (!$loadTerInformation) {
$availableAndInstalledExtensions = $this->listUtility->enrichExtensionsWithEmConfInformation([$extensionKey => $extension]);
} else {
$availableAndInstalledExtensions = $this->listUtility->enrichExtensionsWithEmConfAndTerInformation([$extensionKey => $extension]);
}
if (!isset($availableAndInstalledExtensions[$extensionKey])) {
throw new ExtensionManagerException(
'Please check your uploaded extension "' . $extensionKey . '". The configuration file "ext_emconf.php" seems to be invalid.',
1391432222
);
}
return $availableAndInstalledExtensions[$extensionKey];
}
/**
* @param string $extensionKey
* @return array
* @throws ExtensionManagerException
*/
protected function getExtensionArray($extensionKey)
{
$availableExtensions = $this->listUtility->getAvailableExtensions();
if (isset($availableExtensions[$extensionKey])) {
return $availableExtensions[$extensionKey];
}
throw new ExtensionManagerException('Extension ' . $extensionKey . ' is not available', 1342864081);
}
/**
* Reload Cache files and Typo3LoadedExtensions
*/
public function reloadCaches()
{
$this->reloadOpcache();
ExtensionManagementUtility::loadExtLocalconf(false);
Bootstrap::loadBaseTca(false);
Bootstrap::loadExtTables(false);
}
/**
* Reloads PHP opcache
*/
protected function reloadOpcache()
{
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
}
/**
* Executes all safe database statements.
* Tables and fields are created and altered. Nothing gets deleted or renamed here.
* @internal
*/
public function updateDatabase()
{
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
$schemaMigrator = GeneralUtility::makeInstance(SchemaMigrator::class);
$sqlStatements = [];
$sqlStatements[] = $sqlReader->getTablesDefinitionString();
$sqlStatements = $sqlReader->getCreateTableStatementArray(implode(LF . LF, array_filter($sqlStatements)));
$updateStatements = $schemaMigrator->getUpdateSuggestions($sqlStatements);
$updateStatements = array_merge_recursive(...array_values($updateStatements));
$selectedStatements = [];
foreach (['add', 'change', 'create_table', 'change_table'] as $action) {
if (empty($updateStatements[$action])) {
continue;
}
$statements = array_combine(array_keys($updateStatements[$action]), array_fill(0, count($updateStatements[$action]), true));
$selectedStatements = array_merge(
$selectedStatements,
$statements
);
}
$schemaMigrator->migrate($sqlStatements, $selectedStatements);
}
/**
* Save default configuration of an extension
*
* @param string $extensionKey
*/
protected function saveDefaultConfiguration($extensionKey)
{
$extensionConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class);
$extensionConfiguration->synchronizeExtConfTemplateWithLocalConfiguration($extensionKey);
}
/**
* Import static SQL data (normally used for ext_tables_static+adt.sql)
*
* @param string $rawDefinitions
*/
public function importStaticSql($rawDefinitions)
{
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
$statements = $sqlReader->getStatementArray($rawDefinitions);
$schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
$schemaMigrationService->importStaticData($statements, true);
}
/**
* Remove an extension (delete the directory)
*
* @param string $extension
* @throws ExtensionManagerException
*/
public function removeExtension($extension)
{
$absolutePath = $this->enrichExtensionWithDetails($extension)['packagePath'];
if ($this->isValidExtensionPath($absolutePath)) {
if ($this->packageManager->isPackageAvailable($extension)) {
// Package manager deletes the extension and removes the entry from PackageStates.php
$this->packageManager->deletePackage($extension);
} else {
// The extension is not listed in PackageStates.php, we can safely remove it
$this->fileHandlingUtility->removeDirectory($absolutePath);
}
} else {
throw new ExtensionManagerException('No valid extension path given.', 1342875724);
}
}
/**
* Uses the export import extension to import a T3D or XML file to PID 0
* Execution state is saved in the this->registry, so it only happens once
*
* @param string $extensionKey
* @param string $packagePath
*/
protected function importT3DFile($extensionKey, $packagePath): ?Import
{
$extensionSiteRelPath = PathUtility::stripPathSitePrefix($packagePath);
$registryKeysToCheck = [
$extensionSiteRelPath . 'Initialisation/data.t3d',
$extensionSiteRelPath . 'Initialisation/dataImported',
];
foreach ($registryKeysToCheck as $registryKeyToCheck) {
if ($this->registry->get('extensionDataImport', $registryKeyToCheck)) {
// Data was imported before => early return
return null;
}
}
$importFileToUse = null;
$possibleImportFiles = [
$packagePath . 'Initialisation/data.t3d',
$packagePath . 'Initialisation/data.xml',
];
foreach ($possibleImportFiles as $possibleImportFile) {
if (!file_exists($possibleImportFile)) {
continue;
}
$importFileToUse = $possibleImportFile;
}
if ($importFileToUse !== null) {
$importExportUtility = GeneralUtility::makeInstance(ImportExportUtility::class);
try {
$importResult = $importExportUtility->importT3DFile($importFileToUse, 0);
$this->registry->set('extensionDataImport', $extensionSiteRelPath . 'Initialisation/dataImported', 1);
$this->eventDispatcher->dispatch(new AfterExtensionDatabaseContentHasBeenImportedEvent($extensionKey, $importFileToUse, $importResult, $this));
return $importExportUtility->getImport();
} catch (\ErrorException $e) {
$this->logger->warning($e->getMessage(), ['exception' => $e]);
}
}
return null;
}
/**
* Imports a static tables SQL File (ext_tables_static+adt)
* Execution state is saved in the this->registry, so it only happens once
*
* @param string $packagePath
*/
protected function importStaticSqlFile(string $extensionKey, $packagePath)
{
$extTablesStaticSqlFile = $packagePath . 'ext_tables_static+adt.sql';
$extTablesStaticSqlRelFile = PathUtility::stripPathSitePrefix($extTablesStaticSqlFile);
if (!$this->registry->get('extensionDataImport', $extTablesStaticSqlRelFile)) {
$shortFileHash = '';
if (file_exists($extTablesStaticSqlFile)) {
$extTablesStaticSqlContent = (string)file_get_contents($extTablesStaticSqlFile);
$shortFileHash = md5($extTablesStaticSqlContent);
$this->importStaticSql($extTablesStaticSqlContent);
}
$this->registry->set('extensionDataImport', $extTablesStaticSqlRelFile, $shortFileHash);
$this->eventDispatcher->dispatch(new AfterExtensionStaticDatabaseContentHasBeenImportedEvent($extensionKey, $extTablesStaticSqlFile, $this));
}
}
/**
* Imports files from Initialisation/Files to fileadmin
* via lowlevel copy directory method
*
* @param string $packagePath absolute path to extension dir
* @param string $extensionKey
*/
protected function importInitialFiles($packagePath, $extensionKey)
{
$importFolder = $packagePath . 'Initialisation/Files';
$importRelFolder = PathUtility::stripPathSitePrefix($importFolder);
if (!$this->registry->get('extensionDataImport', $importRelFolder)) {
if (file_exists($importFolder)) {
$destinationAbsolutePath = GeneralUtility::getFileAbsFileName($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] . $extensionKey);
if (!file_exists($destinationAbsolutePath) &&
GeneralUtility::isAllowedAbsPath($destinationAbsolutePath)
) {
GeneralUtility::mkdir($destinationAbsolutePath);
}
GeneralUtility::copyDirectory($importFolder, $destinationAbsolutePath);
$this->registry->set('extensionDataImport', $importRelFolder, 1);
$this->eventDispatcher->dispatch(new AfterExtensionFilesHaveBeenImportedEvent($extensionKey, $destinationAbsolutePath, $this));
}
}
}
/**
* @param Import|null $import
*/
protected function importSiteConfiguration(string $extensionKey, string $packagePath, Import $import = null): void
{
$importAbsFolder = $packagePath . 'Initialisation/Site';
$destinationFolder = Environment::getConfigPath() . '/sites';
if (!is_dir($importAbsFolder)) {
return;
}
$siteConfiguration = GeneralUtility::makeInstance(SiteConfiguration::class);
$existingSites = $siteConfiguration->resolveAllExistingSites(false);
GeneralUtility::mkdir($destinationFolder);
$finder = GeneralUtility::makeInstance(Finder::class);
$finder->directories()->in($importAbsFolder);
if ($finder->hasResults()) {
foreach ($finder as $siteConfigDirectory) {
$siteIdentifier = $siteConfigDirectory->getBasename();
if (isset($existingSites[$siteIdentifier])) {
$this->logger->warning('Skipped importing site configuration from {key} due to existing site identifier {site}', [
'key' => $extensionKey,
'site' => $siteIdentifier,
]);
continue;
}
$targetDir = $destinationFolder . '/' . $siteIdentifier;
if (!$this->registry->get('siteConfigImport', $siteIdentifier) && !is_dir($targetDir)) {
GeneralUtility::mkdir($targetDir);
GeneralUtility::copyDirectory($siteConfigDirectory->getPathname(), $targetDir);
$this->registry->set('siteConfigImport', $siteIdentifier, 1);
}
}
}
$newSites = array_diff_key($siteConfiguration->resolveAllExistingSites(false), $existingSites);
$importedPages = $import?->getImportMapId()['pages'] ?? [];
$newSiteIdentifierList = [];
foreach ($newSites as $newSite) {
$exportedPageId = $newSite->getRootPageId();
$siteIdentifier = $newSite->getIdentifier();
$newSiteIdentifierList[] = $siteIdentifier;
$importedPageId = $importedPages[$exportedPageId] ?? null;
if ($importedPageId === null) {
$this->logger->warning('Imported site configuration with identifier {site} could not be mapped to imported page id', [
'site' => $siteIdentifier,
]);
continue;
}
$configuration = $siteConfiguration->load($siteIdentifier);
$configuration['rootPageId'] = $importedPageId;
try {
$siteConfiguration->write($siteIdentifier, $configuration);
} catch (SiteConfigurationWriteException $e) {
$this->logger->warning(
sprintf(
'Imported site configuration with identifier %s could not be written: %s',
$newSite->getIdentifier(),
$e->getMessage()
)
);
continue;
}
}
$this->eventDispatcher->dispatch(new AfterExtensionSiteFilesHaveBeenImportedEvent($extensionKey, $newSiteIdentifierList));
}
/**
* Is the given path a valid path for extension installation
*
* @param string $path the absolute (!) path in question
*/
protected function isValidExtensionPath($path): bool
{
$allowedPaths = Extension::returnInstallPaths();
foreach ($allowedPaths as $allowedPath) {
if (str_starts_with($path, $allowedPath)) {
return true;
}
}
return false;
}
}