first commit

This commit is contained in:
salam committed 2026-10-19 08:18:39 +02:00
commit 1218dd1517
12716 files changed
+989233

No files matched your search

@@ -0,0 +1,84 @@
<?php
namespace TYPO3\CMS\Install\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\Core\Bootstrap;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Basic service to clear caches within the install tool.
* @internal This is NOT an API class, it is for internal use in the install tool only.
*/
class ClearCacheService
{
/**
* This clear cache implementation follows a pretty brutal approach.
* Goal is to reliably get rid of cache entries, even if some broken
* extension is loaded that would kill the backend 'clear cache' action.
*
* Therefor this method "knows" implementation details of the cache
* framework and uses them to clear all file based cache (typo3temp/Cache)
* and database caches (tables prefixed with cf_) manually.
*
* After that ext_tables and ext_localconf of extensions are loaded, those
* may register additional caches in the caching framework with different
* backend, and will then clear them with the usual flush() method.
*/
public function clearAll()
{
// Delete typo3temp/Cache
GeneralUtility::flushDirectory(Environment::getVarPath() . '/cache', true, true);
// Get all table names from Default connection starting with 'cf_' and truncate them
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$connection = $connectionPool->getConnectionByName('Default');
$tableNames = $connection->getSchemaManager()->listTableNames();
foreach ($tableNames as $tableName) {
if (strpos($tableName, 'cf_') === 0 || $tableName === 'cache_treelist') {
$connection->truncate($tableName);
}
}
// check tables on other connections
$remappedTables = isset($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
? array_keys((array)$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
: [];
foreach ($remappedTables as $tableName) {
if (strpos((string)$tableName, 'cf_') === 0 || $tableName === 'cache_treelist') {
$connectionPool->getConnectionForTable($tableName)->truncate($tableName);
}
}
// From this point on, the code may fatal, if some broken extension is loaded.
// Use bootstrap to load all ext_localconf and ext_tables
Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false);
Bootstrap::unsetReservedGlobalVariables();
Bootstrap::loadBaseTca(false);
Bootstrap::loadExtTables(false);
// The cache manager is already instantiated in the install tool
// with some hacked settings to disable caching of extbase and fluid.
// We want a "fresh" object here to operate on a different cache setup.
// cacheManager implements SingletonInterface, so the only way to get a "fresh"
// instance is by circumventing makeInstance and/or the objectManager and
// using new directly!
$cacheManager = new \TYPO3\CMS\Core\Cache\CacheManager();
$cacheManager->setCacheConfigurations($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']);
$cacheManager->flushCaches();
}
}
@@ -0,0 +1,111 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\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\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Service handling clearing and statistics of semi-persistent
* core tables.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class ClearTableService
{
/**
* @var array List of table and their description
*/
protected $tableList = [
[
'name' => 'be_sessions',
'description' => 'Backend user sessions'
],
[
'name' => 'fe_sessions',
'description' => 'Frontend user sessions',
],
[
'name' => 'sys_history',
'description' => 'Tracking of database record changes through TYPO3 backend forms',
],
[
'name' => 'sys_lockedrecords',
'description' => 'Record locking of backend user editing',
],
[
'name' => 'sys_log',
'description' => 'General log table',
],
[
'name' => 'sys_preview',
'description' => 'Workspace preview links',
],
[
'name' => 'tx_extensionmanager_domain_model_extension',
'description' => 'List of TER extensions',
],
[
'name' => 'tx_rsaauth_keys',
'description' => 'Login process key storage'
],
];
/**
* Get an array of all affected tables, a short description and their row counts
*
* @return array Details per table
*/
public function getTableStatistics(): array
{
$tableStatistics = [];
foreach ($this->tableList as $table) {
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table['name']);
if ($connection->getSchemaManager()->tablesExist([$table['name']])) {
$table['rowCount'] = $connection->count(
'*',
$table['name'],
[]
);
$tableStatistics[] = $table;
}
}
return $tableStatistics;
}
/**
* Truncate a table from $this->tableList
*
* @param string $tableName
* @throws \RuntimeException
*/
public function clearSelectedTable(string $tableName)
{
$tableFound = false;
foreach ($this->tableList as $table) {
if ($table['name'] === $tableName) {
$tableFound = true;
break;
}
}
if (!$tableFound) {
throw new \RuntimeException(
'Selected table ' . $tableName . ' can not be cleared',
1501942151
);
}
GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName)->truncate($tableName);
}
}
@@ -0,0 +1,562 @@
<?php
namespace TYPO3\CMS\Install\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\Core\Environment;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Service\OpcodeCacheService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Install\CoreVersion\CoreRelease;
use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
use TYPO3\CMS\Install\Service\Exception\RemoteFetchException;
/**
* Core update service.
* This service handles core updates, all the nasty details are encapsulated
* here. The single public methods 'depend' on each other, for example a new
* core has to be downloaded before it can be unpacked.
*
* Each method returns only TRUE of FALSE indicating if it was successful or
* not. Detailed information can be fetched with getMessages() and will return
* a list of status messages of the previous operation.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class CoreUpdateService
{
/**
* @var \TYPO3\CMS\Install\Service\CoreVersionService
*/
protected $coreVersionService;
/**
* @var FlashMessageQueue
*/
protected $messages;
/**
* Absolute path to download location
*
* @var string
*/
protected $downloadTargetPath;
/**
* Absolute path to the symlink pointing to the currently used TYPO3 core files
*
* @var string
*/
protected $symlinkToCoreFiles;
/**
* Base URI for TYPO3 downloads
*
* @var string
*/
protected $downloadBaseUri;
/**
* @param CoreVersionService $coreVersionService
*/
public function __construct(CoreVersionService $coreVersionService = null)
{
$this->coreVersionService = $coreVersionService ?: GeneralUtility::makeInstance(CoreVersionService::class);
$this->setDownloadTargetPath(Environment::getVarPath() . '/transient/');
$this->symlinkToCoreFiles = $this->discoverCurrentCoreSymlink();
$this->downloadBaseUri = 'https://get.typo3.org';
$this->messages = new FlashMessageQueue('install');
}
/**
* Check if this installation wants to enable the core updater
*
* @return bool
*/
public function isCoreUpdateEnabled()
{
$coreUpdateDisabled = getenv('TYPO3_DISABLE_CORE_UPDATER') ?: (getenv('REDIRECT_TYPO3_DISABLE_CORE_UPDATER') ?: false);
return !Environment::isComposerMode() && !$coreUpdateDisabled;
}
/**
* In future implementations we might implement some smarter logic here
*
* @return string
*/
protected function discoverCurrentCoreSymlink()
{
return Environment::getPublicPath() . '/typo3_src';
}
/**
* Create download location in case the folder does not exist
* @todo move this to folder structure
*
* @param string $downloadTargetPath
*/
protected function setDownloadTargetPath($downloadTargetPath)
{
if (!is_dir($downloadTargetPath)) {
GeneralUtility::mkdir_deep($downloadTargetPath);
}
$this->downloadTargetPath = $downloadTargetPath;
}
/**
* Get messages of previous method call
*
* @return FlashMessageQueue
*/
public function getMessages(): FlashMessageQueue
{
return $this->messages;
}
/**
* Wrapper method for CoreVersionService
*
* @deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0 - use REST api directly (see https://get.typo3.org/v1/api/doc)
* @return bool TRUE on success
*/
public function updateVersionMatrix()
{
trigger_error(
'The method updateVersionMatrix() is deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0, use the REST api directly (see https://get.typo3.org/v1/api/doc).',
E_USER_DEPRECATED
);
$success = true;
try {
$this->coreVersionService->getYoungestPatchRelease();
} catch (RemoteFetchException $e) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'Current version specification could not be fetched from https://get.typo3.org.'
. ' This is probably a network issue, please fix it.',
'Version information could not be fetched from get.typo3.org',
FlashMessage::ERROR
));
}
return $success;
}
/**
* Check if an update is possible at all
*
* @param CoreRelease $coreRelease The target core release
* @return bool TRUE on success
*/
public function checkPreConditions(CoreRelease $coreRelease)
{
$success = true;
// Folder structure test: Update can be done only if folder structure returns no errors
$folderStructureFacade = GeneralUtility::makeInstance(DefaultFactory::class)->getStructure();
$folderStructureMessageQueue = $folderStructureFacade->getStatus();
$folderStructureErrors = $folderStructureMessageQueue->getAllMessages(FlashMessage::ERROR);
$folderStructureWarnings = $folderStructureMessageQueue->getAllMessages(FlashMessage::WARNING);
if (!empty($folderStructureErrors) || !empty($folderStructureWarnings) || !is_link(Environment::getPublicPath() . '/typo3_src')) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'To perform an update, the folder structure of this TYPO3 CMS instance must'
. ' stick to the conventions, or the update process could lead to unexpected'
. ' results and may be hazardous to your system',
'Automatic TYPO3 CMS core update not possible: Folder structure has errors or warnings',
FlashMessage::ERROR
));
}
// No core update on windows
if (Environment::isWindows()) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'Automatic TYPO3 CMS core update not possible: Update not supported on Windows OS',
FlashMessage::ERROR
));
}
if ($success) {
// Explicit write check to document root
$file = Environment::getPublicPath() . '/' . StringUtility::getUniqueId('install-core-update-test-');
$result = @touch($file);
if (!$result) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'Could not write a file in path "' . Environment::getPublicPath() . '/"!',
'Automatic TYPO3 CMS core update not possible: No write access to document root',
FlashMessage::ERROR
));
} else {
// Check symlink creation
$link = Environment::getPublicPath() . '/' . StringUtility::getUniqueId('install-core-update-test-');
@symlink($file, $link);
if (!is_link($link)) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'Could not create a symbolic link in path "' . Environment::getPublicPath() . '/"!',
'Automatic TYPO3 CMS core update not possible: No symlink creation possible',
FlashMessage::ERROR
));
} else {
unlink($link);
}
unlink($file);
}
if (!$this->checkCoreFilesAvailable($coreRelease->getVersion())) {
// Explicit write check to upper directory of current core location
$coreLocation = @realpath($this->symlinkToCoreFiles . '/../');
$file = $coreLocation . '/' . StringUtility::getUniqueId('install-core-update-test-');
$result = @touch($file);
if (!$result) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'New TYPO3 CMS core should be installed in "' . $coreLocation . '", but this directory is not writable!',
'Automatic TYPO3 CMS core update not possible: No write access to TYPO3 CMS core location',
FlashMessage::ERROR
));
} else {
unlink($file);
}
}
}
if ($success && !$this->coreVersionService->isInstalledVersionAReleasedVersion()) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'Your current version is specified as ' . $this->coreVersionService->getInstalledVersion() . '.'
. ' This is a development version and can not be updated automatically. If this is a "git"'
. ' checkout, please update using git directly.',
'Automatic TYPO3 CMS core update not possible: You are running a development version of TYPO3',
FlashMessage::ERROR
));
}
return $success;
}
/**
* Download the specified version
*
* @param CoreRelease $coreRelease A core release to download
* @return bool TRUE on success
*/
public function downloadVersion(CoreRelease $coreRelease)
{
$version = $coreRelease->getVersion();
$success = true;
if ($this->checkCoreFilesAvailable($version)) {
$this->messages->enqueue(new FlashMessage(
'',
'Skipped download of TYPO3 CMS core. A core source directory already exists in destination path. Using this instead.',
FlashMessage::NOTICE
));
} else {
$downloadUri = $this->downloadBaseUri . '/' . $version;
$fileLocation = $this->getDownloadTarGzTargetPath($version);
if (@file_exists($fileLocation)) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'TYPO3 CMS core download exists in download location: ' . PathUtility::stripPathSitePrefix($this->downloadTargetPath),
FlashMessage::ERROR
));
} else {
$fileContent = GeneralUtility::getUrl($downloadUri);
if (!$fileContent) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'Failed to download ' . $downloadUri,
'Download not successful',
FlashMessage::ERROR
));
} else {
$fileStoreResult = file_put_contents($fileLocation, $fileContent);
if (!$fileStoreResult) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'Unable to store download content',
FlashMessage::ERROR
));
} else {
$this->messages->enqueue(new FlashMessage(
'',
'TYPO3 CMS core download finished'
));
}
}
}
}
return $success;
}
/**
* Verify checksum of downloaded version
*
* @param CoreRelease $coreRelease A downloaded core release to check
* @return bool TRUE on success
*/
public function verifyFileChecksum(CoreRelease $coreRelease)
{
$version = $coreRelease->getVersion();
$success = true;
if ($this->checkCoreFilesAvailable($version)) {
$this->messages->enqueue(new FlashMessage(
'',
'Verifying existing TYPO3 CMS core checksum is not possible',
FlashMessage::WARNING
));
} else {
$fileLocation = $this->getDownloadTarGzTargetPath($version);
$expectedChecksum = $coreRelease->getChecksum();
if (!file_exists($fileLocation)) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'Downloaded TYPO3 CMS core not found',
FlashMessage::ERROR
));
} else {
$actualChecksum = sha1_file($fileLocation);
if ($actualChecksum !== $expectedChecksum) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'The official TYPO3 CMS version system on https://get.typo3.org expects a sha1 checksum of '
. $expectedChecksum . ' from the content of the downloaded new TYPO3 CMS core version ' . $version . '.'
. ' The actual checksum is ' . $actualChecksum . '. The update is stopped. This may be a'
. ' failed download, an attack, or an issue with the typo3.org infrastructure.',
'New TYPO3 CMS core checksum mismatch',
FlashMessage::ERROR
));
} else {
$this->messages->enqueue(new FlashMessage(
'',
'Checksum verified'
));
}
}
}
return $success;
}
/**
* Unpack a downloaded core
*
* @param CoreRelease $coreRelease A core release to unpack
* @return bool TRUE on success
*/
public function unpackVersion(CoreRelease $coreRelease)
{
$version = $coreRelease->getVersion();
$success = true;
if ($this->checkCoreFilesAvailable($version)) {
$this->messages->enqueue(new FlashMessage(
'',
'Unpacking TYPO3 CMS core files skipped',
FlashMessage::NOTICE
));
} else {
$fileLocation = $this->downloadTargetPath . $version . '.tar.gz';
if (!@is_file($fileLocation)) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'Downloaded TYPO3 CMS core not found',
FlashMessage::ERROR
));
} elseif (@file_exists($this->downloadTargetPath . 'typo3_src-' . $version)) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'Unpacked TYPO3 CMS core exists in download location: ' . PathUtility::stripPathSitePrefix($this->downloadTargetPath),
FlashMessage::ERROR
));
} else {
$unpackCommand = 'tar xf ' . escapeshellarg($fileLocation) . ' -C ' . escapeshellarg($this->downloadTargetPath) . ' 2>&1';
exec($unpackCommand, $output, $errorCode);
if ($errorCode) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'Unpacking TYPO3 CMS core not successful',
FlashMessage::ERROR
));
} else {
$removePackedFileResult = unlink($fileLocation);
if (!$removePackedFileResult) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'Removing packed TYPO3 CMS core not successful',
FlashMessage::ERROR
));
} else {
$this->messages->enqueue(new FlashMessage(
'',
'Unpacking TYPO3 CMS core successful'
));
}
}
}
}
return $success;
}
/**
* Move an unpacked core to its final destination
*
* @param CoreRelease $coreRelease A core release to move
* @return bool TRUE on success
*/
public function moveVersion(CoreRelease $coreRelease)
{
$version = $coreRelease->getVersion();
$success = true;
if ($this->checkCoreFilesAvailable($version)) {
$this->messages->enqueue(new FlashMessage(
'',
'Moving TYPO3 CMS core files skipped',
FlashMessage::NOTICE
));
} else {
$downloadedCoreLocation = $this->downloadTargetPath . 'typo3_src-' . $version;
$newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
if (!@is_dir($downloadedCoreLocation)) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'Unpacked TYPO3 CMS core not found',
FlashMessage::ERROR
));
} else {
$moveResult = rename($downloadedCoreLocation, $newCoreLocation);
if (!$moveResult) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'Moving TYPO3 CMS core to ' . $newCoreLocation . ' failed',
FlashMessage::ERROR
));
} else {
$this->messages->enqueue(new FlashMessage(
'',
'Moved TYPO3 CMS core to final location'
));
}
}
}
return $success;
}
/**
* Activate a core version
*
* @param CoreRelease $coreRelease A core release to activate
* @return bool TRUE on success
*/
public function activateVersion(CoreRelease $coreRelease)
{
$newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $coreRelease->getVersion();
$success = true;
if (!is_dir($newCoreLocation)) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'New TYPO3 CMS core not found',
FlashMessage::ERROR
));
} elseif (!is_link($this->symlinkToCoreFiles)) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'TYPO3 CMS core source directory (typo3_src) is not a link',
FlashMessage::ERROR
));
} else {
$isCurrentCoreSymlinkAbsolute = PathUtility::isAbsolutePath(readlink($this->symlinkToCoreFiles));
$unlinkResult = unlink($this->symlinkToCoreFiles);
if (!$unlinkResult) {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'Removing old symlink failed',
FlashMessage::ERROR
));
} else {
if (!$isCurrentCoreSymlinkAbsolute) {
$newCoreLocation = $this->getRelativePath($newCoreLocation);
}
$symlinkResult = symlink($newCoreLocation, $this->symlinkToCoreFiles);
if ($symlinkResult) {
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
} else {
$success = false;
$this->messages->enqueue(new FlashMessage(
'',
'Linking new TYPO3 CMS core failed',
FlashMessage::ERROR
));
}
}
}
return $success;
}
/**
* Absolute path of downloaded .tar.gz
*
* @param string $version A version number
* @return string
*/
protected function getDownloadTarGzTargetPath($version)
{
return $this->downloadTargetPath . $version . '.tar.gz';
}
/**
* Get relative path to TYPO3 source directory from webroot
*
* @param string $absolutePath to TYPO3 source directory
* @return string relative path to TYPO3 source directory
*/
protected function getRelativePath($absolutePath)
{
$sourcePath = explode(DIRECTORY_SEPARATOR, Environment::getPublicPath());
$targetPath = explode(DIRECTORY_SEPARATOR, rtrim($absolutePath, DIRECTORY_SEPARATOR));
while (count($sourcePath) && count($targetPath) && $sourcePath[0] === $targetPath[0]) {
array_shift($sourcePath);
array_shift($targetPath);
}
return str_pad('', count($sourcePath) * 3, '..' . DIRECTORY_SEPARATOR) . implode(DIRECTORY_SEPARATOR, $targetPath);
}
/**
* Check if there is are already core files available
* at the download destination.
*
* @param string $version A version number
* @return bool true when core files are available
*/
protected function checkCoreFilesAvailable($version)
{
$newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
return @is_dir($newCoreLocation);
}
}
@@ -0,0 +1,335 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\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\Registry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
use TYPO3\CMS\Install\CoreVersion\CoreRelease;
use TYPO3\CMS\Install\CoreVersion\MaintenanceWindow;
use TYPO3\CMS\Install\CoreVersion\MajorRelease;
/**
* Core version service
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class CoreVersionService
{
/**
* @var \TYPO3\CMS\Core\Registry
*/
protected $registry;
/**
* Base URI for TYPO3 Version REST api
*
* @var string
*/
protected $apiBaseUrl = 'https://get.typo3.org/v1/api/';
/**
* Initialize update URI
*
* @param Registry $registry Deprecated
*/
public function __construct(Registry $registry = null)
{
if (null !== $registry) {
trigger_error(
'The constructor parameter $registry for CoreVersionService is deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0.',
E_USER_DEPRECATED
);
$this->registry = $registry;
} else {
$this->registry = GeneralUtility::makeInstance(Registry::class);
}
}
/**
* Development git checkout versions always end with '-dev'. They are
* not "released" as such and can not be updated.
*
* @return bool FALSE If some development version is installed
*/
public function isInstalledVersionAReleasedVersion(): bool
{
$version = $this->getInstalledVersion();
return substr($version, -4) !== '-dev';
}
/**
* Get current installed version number
*
* @return string
*/
public function getInstalledVersion(): string
{
return VersionNumberUtility::getCurrentTypo3Version();
}
public function getMaintenanceWindow(): MaintenanceWindow
{
$url = 'major/' . $this->getInstalledMajorVersion();
$result = $this->fetchFromRemote($url);
return MaintenanceWindow::fromApiResponse($result);
}
/**
* @return array{community: string[], elts: string[]}
*/
public function getSupportedMajorReleases(): array
{
$url = 'major';
$result = $this->fetchFromRemote($url);
$majorReleases = [
'community' => [],
'elts' => [],
];
foreach ($result as $release) {
$majorRelease = MajorRelease::fromApiResponse($release);
$maintenanceWindow = $majorRelease->getMaintenanceWindow();
if ($maintenanceWindow->isSupportedByCommunity()) {
$group = 'community';
} elseif ($maintenanceWindow->isSupportedByElts()) {
$group = 'elts';
} else {
// Major version is unsupported
continue;
}
$majorReleases[$group][] = $majorRelease->getLts() ?? $majorRelease->getVersion();
}
return $majorReleases;
}
public function isPatchReleaseSuitableForUpdate(CoreRelease $coreRelease): bool
{
return version_compare($this->getInstalledVersion(), $coreRelease->getVersion()) === -1;
}
/**
* Returns TRUE if a younger patch level release exists in version matrix.
*
* @return bool TRUE if younger patch release is exists
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
*/
public function isYoungerPatchReleaseAvailable(): bool
{
return version_compare($this->getInstalledVersion(), $this->getYoungestPatchRelease()->getVersion()) === -1;
}
/**
* Returns TRUE if an upgrade from current version is security relevant
*
* @return bool TRUE if there is a pending security update
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
*/
public function isUpdateSecurityRelevant(CoreRelease $releaseToCheck): bool
{
$url = 'major/' . $this->getInstalledMajorVersion() . '/release';
$result = $this->fetchFromRemote($url);
$installedVersion = $this->getInstalledVersion();
foreach ($result as $release) {
$coreRelease = CoreRelease::fromApiResponse($release);
if ($coreRelease->isSecurityUpdate()
&& version_compare($installedVersion, $coreRelease->getVersion()) === -1 // installed version is lower than release
&& version_compare($releaseToCheck->getVersion(), $coreRelease->getVersion()) > -1 // release to check is equal or higher than release
) {
return true;
}
}
return false;
}
public function isCurrentInstalledVersionElts(): bool
{
$url = 'major/' . $this->getInstalledMajorVersion() . '/release';
$result = $this->fetchFromRemote($url);
$installedVersion = $this->getInstalledVersion();
foreach ($result as $release) {
if (version_compare($installedVersion, $release['version']) === 0) {
return $release['elts'] ?? false;
}
}
return false;
}
/**
* Youngest patch release
*
* @return CoreRelease
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
*/
public function getYoungestPatchRelease(): CoreRelease
{
$url = 'major/' . $this->getInstalledMajorVersion() . '/release/latest';
$result = $this->fetchFromRemote($url);
return CoreRelease::fromApiResponse($result);
}
public function getYoungestCommunityPatchRelease(): CoreRelease
{
$url = 'major/' . $this->getInstalledMajorVersion() . '/release';
$result = $this->fetchFromRemote($url);
// Make sure all releases are sorted by their version
$columns = array_column($result, 'version');
array_multisort($columns, SORT_NATURAL, $result);
// Remove any ELTS release
$releases = array_filter($result, static function (array $release) {
return ($release['elts'] ?? false) === false;
});
$latestRelease = end($releases);
return CoreRelease::fromApiResponse($latestRelease);
}
/**
* @param string $url
* @return array
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
*/
protected function fetchFromRemote(string $url): array
{
$url = $this->apiBaseUrl . $url;
$json = GeneralUtility::getUrl($url);
if (!$json) {
$this->throwFetchException($url);
}
return json_decode($json, true);
}
/**
* Get 'major version' from installed version of TYPO3, e.g., '7' from '7.3.0'
*
* @return string For example 7
*/
protected function getInstalledMajorVersion(): string
{
return $this->getMajorVersion($this->getInstalledVersion());
}
/**
* Get 'major version' of version, e.g., '7' from '7.3.0'
*
* @param string $version to check
* @return string Major version, e.g., '7'
*/
protected function getMajorVersion(string $version): string
{
$explodedVersion = explode('.', $version);
return $explodedVersion[0];
}
/**
* Update version matrix from remote and store in registry
*
* @deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0 - use new REST API directly (see https://get.typo3.org/v1/api/doc)
* @throws Exception\RemoteFetchException
*/
public function updateVersionMatrix(): void
{
trigger_error('Method updateVersionMatrix will be removed in TYPO3 v10.0, use new REST API directly (see https://get.typo3.org/v1/api/doc).', E_USER_DEPRECATED);
$url = 'https://get.typo3.org/json';
$versionJson = GeneralUtility::getUrl($url);
if (!$versionJson) {
$this->throwFetchException($url);
}
$versionArray = json_decode($versionJson, true);
$installedMajorVersion = (int)$this->getInstalledMajorVersion();
foreach ($versionArray as $versionNumber => $versionDetails) {
if (is_array($versionDetails) && (int)$this->getMajorVersion((string)$versionNumber) < $installedMajorVersion) {
unset($versionArray[$versionNumber]);
}
}
$this->registry->set('TYPO3.CMS.Install', 'coreVersionMatrix', $versionArray);
}
/**
* Youngest development patch release, e.g., 6.2.0alpha3 or 6.2-snapshot-20131004
*
* @deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0 - TYPO3 release cycles do not contain development releases anymore
* @return string
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
*/
public function getYoungestPatchDevelopmentRelease(): string
{
trigger_error(
'Method getYoungestPatchDevelopmentRelease() is deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0, use getYoungestPatchRelease() instead.',
E_USER_DEPRECATED
);
return $this->getYoungestPatchRelease()->getVersion();
}
/**
* Returns TRUE if a younger patch level release exists in version matrix that may be a development release.
*
* @deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0 - TYPO3 release cycles do not contain development releases anymore
* @return bool TRUE if younger patch release is exists
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
*/
public function isYoungerPatchDevelopmentReleaseAvailable(): bool
{
trigger_error(
'Method isYoungerPatchDevelopmentReleaseAvailable() is deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0, use isYoungerPatchReleaseAvailable() instead.',
E_USER_DEPRECATED
);
return $this->isYoungerPatchReleaseAvailable();
}
/**
* @deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0, use 'https://get.typo3.org' directly
* @return string
*/
public function getDownloadBaseUrl(): string
{
trigger_error(
'Method getDownloadBaseUrl() is deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0, use https://get.typo3.org directly.',
E_USER_DEPRECATED
);
return $this->apiBaseUrl;
}
/**
* Helper method to throw same exception in multiple places
*
* @param string $url
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
*/
protected function throwFetchException(string $url): void
{
throw new Exception\RemoteFetchException(
'Fetching ' .
$url .
' failed. Maybe this instance can not connect to the remote system properly.',
1380897593
);
}
}
@@ -0,0 +1,194 @@
<?php
namespace TYPO3\CMS\Install\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\Core\Environment;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Basic Service to check and create install tool files
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class EnableFileService
{
/**
* @var string Relative path to ENABLE_INSTALL_TOOL file
*/
const INSTALL_TOOL_ENABLE_FILE_PATH = 'typo3conf/ENABLE_INSTALL_TOOL';
/**
* @var string Relative path to FIRST_INSTALL file
*/
const FIRST_INSTALL_FILE_PATH = 'FIRST_INSTALL';
/**
* @var string Maximum age of ENABLE_INSTALL_TOOL file before it gets removed (in seconds)
*/
const INSTALL_TOOL_ENABLE_FILE_LIFETIME = 3600;
/**
* @return bool
*/
public static function isFirstInstallAllowed()
{
$files = self::getFirstInstallFilePaths();
if (!empty($files)) {
return true;
}
return false;
}
/**
* Creates the INSTALL_TOOL_ENABLE file
*
* @return bool
*/
public static function createInstallToolEnableFile()
{
$installEnableFilePath = self::getInstallToolEnableFilePath();
if (!is_file($installEnableFilePath)) {
$result = touch($installEnableFilePath);
} else {
$result = true;
self::extendInstallToolEnableFileLifetime();
}
GeneralUtility::fixPermissions($installEnableFilePath);
return $result;
}
/**
* Removes the INSTALL_TOOL_ENABLE file
*
* @return bool
*/
public static function removeInstallToolEnableFile()
{
return unlink(self::getInstallToolEnableFilePath());
}
/**
* Removes the FIRST_INSTALL file
*
* @return bool
*/
public static function removeFirstInstallFile()
{
$result = true;
$files = self::getFirstInstallFilePaths();
foreach ($files as $file) {
$result = unlink(Environment::getPublicPath() . '/' . $file) && $result;
}
return $result;
}
/**
* Checks if the install tool file exists
*
* @return bool
*/
public static function installToolEnableFileExists()
{
return @is_file(self::getInstallToolEnableFilePath());
}
/**
* Checks if the install tool file exists
*
* @return bool
*/
public static function checkInstallToolEnableFile()
{
if (!self::installToolEnableFileExists()) {
return false;
}
if (!self::isInstallToolEnableFilePermanent()) {
if (self::installToolEnableFileLifetimeExpired()) {
self::removeInstallToolEnableFile();
return false;
}
self::extendInstallToolEnableFileLifetime();
}
return true;
}
/**
* Checks if the install tool file should be kept
*
* @return bool
*/
public static function isInstallToolEnableFilePermanent()
{
if (self::installToolEnableFileExists()) {
$content = @file_get_contents(self::getInstallToolEnableFilePath());
if (strpos($content, 'KEEP_FILE') !== false) {
return true;
}
}
return false;
}
/**
* Checks if the lifetime of the install tool file is expired
*
* @return bool
*/
public static function installToolEnableFileLifetimeExpired()
{
if (time() - @filemtime(self::getInstallToolEnableFilePath()) > self::INSTALL_TOOL_ENABLE_FILE_LIFETIME) {
return true;
}
return false;
}
/**
* Updates the last modification of the ENABLE_INSTALL_TOOL file
*/
protected static function extendInstallToolEnableFileLifetime()
{
$enableFile = self::getInstallToolEnableFilePath();
// Extend the age of the ENABLE_INSTALL_TOOL file by one hour
if (is_file($enableFile)) {
$couldTouch = @touch($enableFile);
if (!$couldTouch) {
// If we can't remove the creation method will call us again.
if (self::removeInstallToolEnableFile()) {
self::createInstallToolEnableFile();
}
}
}
}
/**
* Returns the path to the INSTALL_TOOL_ENABLE file
*
* @return string
*/
protected static function getInstallToolEnableFilePath()
{
return Environment::getPublicPath() . '/' . self::INSTALL_TOOL_ENABLE_FILE_PATH;
}
/**
* Returns the paths to the FIRST_INSTALL files
*
* @return array
*/
protected static function getFirstInstallFilePaths()
{
$files = array_filter(scandir(Environment::getPublicPath() . '/'), function ($file) {
return @is_file(Environment::getPublicPath() . '/' . $file) && preg_match('~^' . self::FIRST_INSTALL_FILE_PATH . '.*~i', $file);
});
return $files;
}
}
@@ -0,0 +1,22 @@
<?php
namespace TYPO3\CMS\Install\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!
*/
/**
* A service exception
*/
class Exception extends \TYPO3\CMS\Install\Exception
{
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Service\Exception;
/*
* 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\Install\Service\Exception;
/**
* An exception thrown if the silent configuration updater changed configuration
*/
class ConfigurationChangedException extends Exception
{
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Service\Exception;
/*
* 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\Install\Service\Exception;
/**
* An exception thrown if version validation against official version matrix fails
*/
class CoreVersionServiceException extends Exception
{
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Service\Exception;
/*
* 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!
*/
/**
* An exception thrown if fetching a resource from a remote server fails
*/
class RemoteFetchException extends CoreVersionServiceException
{
}
@@ -0,0 +1,351 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\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\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Service to prepare extension configuration settings from ext_conf_template.txt
* to be viewed in the install tool. The class basically adds display related
* stuff on top of ext:core ExtensionConfiguration.
*
* Extension authors should use TYPO3\CMS\Core\Configuration\ExtensionConfiguration
* class to get() extension configuration settings.
*
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class ExtensionConfigurationService
{
/**
* This will be filled with the available categories of the current template.
* Used parsing ext_conf_template.txt
*
* @var array
*/
protected $subCategories = [
// Standard categories:
'enable' => ['Enable features', 'a'],
'dims' => ['Dimensions, widths, heights, pixels', 'b'],
'file' => ['Files', 'c'],
'typo' => ['Typography', 'd'],
'color' => ['Colors', 'e'],
'links' => ['Links and targets', 'f'],
'language' => ['Language specific constants', 'g'],
// subcategories based on the default content elements
'cheader' => ['Content: \'Header\'', 'ma'],
'cheader_g' => ['Content: \'Header\', Graphical', 'ma'],
'ctext' => ['Content: \'Text\'', 'mb'],
'cimage' => ['Content: \'Image\'', 'md'],
'ctextmedia' => ['Content: \'Textmedia\'', 'ml'],
'cbullets' => ['Content: \'Bullet list\'', 'me'],
'ctable' => ['Content: \'Table\'', 'mf'],
'cuploads' => ['Content: \'Filelinks\'', 'mg'],
'cmultimedia' => ['Content: \'Multimedia\'', 'mh'],
'cmedia' => ['Content: \'Media\'', 'mr'],
'cmailform' => ['Content: \'Form\'', 'mi'],
'csearch' => ['Content: \'Search\'', 'mj'],
'clogin' => ['Content: \'Login\'', 'mk'],
'cmenu' => ['Content: \'Menu/Sitemap\'', 'mm'],
'cshortcut' => ['Content: \'Insert records\'', 'mn'],
'clist' => ['Content: \'List of records\'', 'mo'],
'chtml' => ['Content: \'HTML\'', 'mq']
];
/**
* Compiles ext_conf_template file and merges it with values from LocalConfiguration['EXTENSIONS'].
* Returns a funny array used to display the configuration form in the install tool.
*
* @param string $extensionKey Extension key
* @return array
*/
public function getConfigurationPreparedForView(string $extensionKey): array
{
$package = GeneralUtility::makeInstance(PackageManager::class)->getPackage($extensionKey);
if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
return [];
}
$extensionConfiguration = new ExtensionConfiguration();
$configuration = $this->getDefaultConfigurationFromExtConfTemplateAsValuedArray($extensionKey);
foreach ($configuration as $configurationPath => &$details) {
try {
$valueFromLocalConfiguration = $extensionConfiguration->get($extensionKey, str_replace('.', '/', $configurationPath));
$details['value'] = $valueFromLocalConfiguration;
} catch (ExtensionConfigurationPathDoesNotExistException $e) {
// Deliberately empty - it can happen at runtime that a written config does not return
// back all values (eg. saltedpassword with its userFuncs), which then miss in the written
// configuration and are only synced after next install tool run. This edge case is
// taken care off here.
}
}
$resultArray = [];
if (!empty($configuration)) {
$hierarchicConfiguration = [];
foreach ($configuration as $configurationOption) {
$originalConfiguration = $this->buildConfigurationArray($configurationOption);
ArrayUtility::mergeRecursiveWithOverrule($originalConfiguration, $hierarchicConfiguration);
$hierarchicConfiguration = $originalConfiguration;
}
// Flip category array as it was merged the other way around
$hierarchicConfiguration = array_reverse($hierarchicConfiguration);
// Sort configurations of each subcategory
foreach ($hierarchicConfiguration as &$catConfigurationArray) {
foreach ($catConfigurationArray as &$subcatConfigurationArray) {
uasort($subcatConfigurationArray, function ($a, $b) {
return strnatcmp($a['subcat'], $b['subcat']);
});
}
unset($subcatConfigurationArray);
}
unset($tempConfiguration);
$resultArray = $hierarchicConfiguration;
}
return $resultArray;
}
/**
* Builds a configuration array from each line (option) of the config file.
* Helper method for getConfigurationPreparedForView()
*
* @param array $configurationOption config file line representing one setting
* @return array
*/
protected function buildConfigurationArray(array $configurationOption): array
{
$hierarchicConfiguration = [];
if (GeneralUtility::isFirstPartOfStr($configurationOption['type'], 'user')) {
$configurationOption = $this->extractInformationForConfigFieldsOfTypeUser($configurationOption);
} elseif (GeneralUtility::isFirstPartOfStr($configurationOption['type'], 'options')) {
$configurationOption = $this->extractInformationForConfigFieldsOfTypeOptions($configurationOption);
}
$languageService = $this->getLanguageService();
if (is_string($configurationOption['label'])) {
$translatedLabel = $languageService->sL($configurationOption['label']);
if ($translatedLabel) {
$configurationOption['label'] = $translatedLabel;
}
}
$configurationOption['labels'] = GeneralUtility::trimExplode(':', $configurationOption['label'], false, 2);
$configurationOption['subcat_name'] = $configurationOption['subcat_name'] ?: '__default';
$hierarchicConfiguration[$configurationOption['cat']][$configurationOption['subcat_name']][$configurationOption['name']] = $configurationOption;
return $hierarchicConfiguration;
}
/**
* Extracts additional information for fields of type "options"
* Extracts "type", "label" and values information
*
* @param array $configurationOption
* @return array
*/
protected function extractInformationForConfigFieldsOfTypeOptions(array $configurationOption): array
{
preg_match('/options\[(.*)\]/is', $configurationOption['type'], $typeMatches);
$optionItems = GeneralUtility::trimExplode(',', $typeMatches[1]);
foreach ($optionItems as $optionItem) {
$optionPair = GeneralUtility::trimExplode('=', $optionItem);
if (count($optionPair) === 2) {
$configurationOption['generic'][$optionPair[0]] = $optionPair[1];
} else {
$configurationOption['generic'][$optionPair[0]] = $optionPair[0];
}
}
$configurationOption['type'] = 'options';
return $configurationOption;
}
/**
* Extract additional information for fields of type "user"
* Extracts "type" and the function to be called
*
* @param array $configurationOption
* @return array
*/
protected function extractInformationForConfigFieldsOfTypeUser(array $configurationOption): array
{
preg_match('/user\\[(.*)\\]/is', $configurationOption['type'], $matches);
$configurationOption['generic'] = $matches[1];
$configurationOption['type'] = 'user';
return $configurationOption;
}
/**
* Create a flat array of configuration options from
* ext_conf_template.txt of an extension using core's typoscript parser.
*
* Result is an array, with configuration item as array keys,
* and item properties as key-value sub-array:
*
* array(
* 'fooOption' => array(
* 'type' => 'string',
* 'value' => 'foo',
* ...
* ),
* 'barOption' => array(
* 'type' => boolean,
* 'default_value' => 0,
* ...
* ),
* ...
* )
*
* @param string $extensionKey Extension key
* @return array
*/
protected function getDefaultConfigurationFromExtConfTemplateAsValuedArray(string $extensionKey): array
{
$parsedConstants = (new ExtensionConfiguration())->getParsedExtConfTemplate($extensionKey);
$flatSetup = $this->flattenSetup($parsedConstants);
$theConstants = $this->parseComments($flatSetup);
// Loop through configuration items, see if it is assigned to a sub category
// and add the sub category label to the item property if so.
foreach ($theConstants as $configurationOptionName => $configurationOption) {
if (
array_key_exists('subcat_name', $configurationOption)
&& isset($this->subCategories[$configurationOption['subcat_name']])
&& isset($this->subCategories[$configurationOption['subcat_name']][0])
) {
$theConstants[$configurationOptionName]['subcat_label'] = $this->subCategories[$configurationOption['subcat_name']][0];
}
$theConstants[$configurationOptionName]['extensionKey'] = $extensionKey;
}
return $theConstants;
}
/**
* This function compares the flattened constants (default and all).
* Returns an array with the constants from the whole template which may be edited by the module.
*
* @param array $flatSetup
* @return array
*/
protected function parseComments($flatSetup)
{
$categoryLabels = [];
$editableComments = [];
$counter = 0;
foreach ($flatSetup as $const => $value) {
if (substr($const, -2) === '..' || !isset($flatSetup[$const . '..'])) {
continue;
}
$counter++;
$comment = trim($flatSetup[$const . '..']);
$c_arr = explode(LF, $comment);
foreach ($c_arr as $k => $v) {
$line = trim(preg_replace('/^[#\\/]*/', '', $v));
if (!$line) {
continue;
}
$parts = explode(';', $line);
foreach ($parts as $par) {
if (strstr($par, '=')) {
$keyValPair = explode('=', $par, 2);
switch (trim(strtolower($keyValPair[0]))) {
case 'type':
// Type:
$editableComments[$const]['type'] = trim($keyValPair[1]);
break;
case 'cat':
// List of categories.
$catSplit = explode('/', strtolower($keyValPair[1]));
$catSplit[0] = trim($catSplit[0]);
if (isset($categoryLabels[$catSplit[0]])) {
$catSplit[0] = $categoryLabels[$catSplit[0]];
}
$editableComments[$const]['cat'] = $catSplit[0];
// This is the subcategory. Must be a key in $this->subCategories[].
// catSplit[2] represents the search-order within the subcat.
$catSplit[1] = !empty($catSplit[1]) ? trim($catSplit[1]) : '';
if ($catSplit[1] && isset($this->subCategories[$catSplit[1]])) {
$editableComments[$const]['subcat_name'] = $catSplit[1];
$orderIdentifier = isset($catSplit[2]) ? trim($catSplit[2]) : $counter;
$editableComments[$const]['subcat'] = $this->subCategories[$catSplit[1]][1]
. '/' . $catSplit[1] . '/' . $orderIdentifier . 'z';
} elseif (isset($catSplit[2])) {
$editableComments[$const]['subcat'] = 'x' . '/' . trim($catSplit[2]) . 'z';
} else {
$editableComments[$const]['subcat'] = 'x' . '/' . $counter . 'z';
}
break;
case 'label':
// Label
$editableComments[$const]['label'] = trim($keyValPair[1]);
break;
case 'customcategory':
// Custom category label
$customCategory = explode('=', $keyValPair[1], 2);
if (trim($customCategory[0])) {
$categoryKey = strtolower($customCategory[0]);
$categoryLabels[$categoryKey] = $this->getLanguageService()->sL($customCategory[1]);
}
break;
case 'customsubcategory':
// Custom subCategory label
$customSubcategory = explode('=', $keyValPair[1], 2);
if (trim($customSubcategory[0])) {
$subCategoryKey = strtolower($customSubcategory[0]);
$this->subCategories[$subCategoryKey][0] = $this->getLanguageService()->sL($customSubcategory[1]);
}
break;
}
}
}
}
if (isset($editableComments[$const])) {
$editableComments[$const]['name'] = $const;
$editableComments[$const]['value'] = trim($value);
$editableComments[$const]['default_value'] = trim($value);
}
}
return $editableComments;
}
/**
* This flattens a hierarchical TypoScript array to a dotted notation
*
* @param array $setupArray TypoScript array
* @param string $prefix Prefix to the object path. Used for recursive calls to this function.
* @return array
*/
protected function flattenSetup($setupArray, $prefix = '')
{
$flatSetup = [];
if (is_array($setupArray)) {
foreach ($setupArray as $key => $val) {
if (is_array($val)) {
$flatSetup = array_merge($flatSetup, $this->flattenSetup($val, $prefix . $key));
} else {
$flatSetup[$prefix . $key] = $val;
}
}
}
return $flatSetup;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,370 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\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 Symfony\Component\Finder\Finder;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Http\RequestFactory;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Service\Archive\ZipService;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
/**
* Service class handling language pack details
* Used by 'manage language packs' module and 'language packs command'
*
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class LanguagePackService
{
/**
* @var Locales
*/
protected $locales;
/**
* @var Registry
*/
protected $registry;
/**
* @var RequestFactory
*/
protected $requestFactory;
private const OLD_LANGUAGE_PACK_URLS = [
'https://extensions.typo3.org/fileadmin/l10n/',
'https://typo3.org/fileadmin/ter/',
'https://beta-translation.typo3.org/fileadmin/ter/',
'https://localize.typo3.org/fileadmin/ter/'
];
private const OLD_LANGUAGE_PACK_URL = 'https://extensions.typo3.org/fileadmin/l10n/';
private const LANGUAGE_PACK_URL = 'https://localize.typo3.org/xliff/';
public function __construct()
{
$this->locales = GeneralUtility::makeInstance(Locales::class);
$this->registry = GeneralUtility::makeInstance(Registry::class);
$this->requestFactory = GeneralUtility::makeInstance(RequestFactory::class);
}
/**
* Get list of available languages
*
* @return array iso=>name
*/
public function getAvailableLanguages(): array
{
return $this->locales->getLanguages();
}
/**
* List of languages active in this instance
*
* @return array
*/
public function getActiveLanguages(): array
{
$availableLanguages = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] ?? [];
return array_filter($availableLanguages);
}
/**
* Create an array with language details: active or not, iso codes, last update, ...
*
* @return array
*/
public function getLanguageDetails(): array
{
$availableLanguages = $this->getAvailableLanguages();
$activeLanguages = $this->getActiveLanguages();
$languages = [];
foreach ($availableLanguages as $iso => $name) {
if ($iso === 'default') {
continue;
}
$lastUpdate = $this->registry->get('languagePacks', $iso);
$languages[] = [
'iso' => $iso,
'name' => $name,
'active' => in_array($iso, $activeLanguages, true),
'lastUpdate' => $this->getFormattedDate($lastUpdate),
'dependencies' => $this->locales->getLocaleDependencies($iso),
];
}
usort($languages, function ($a, $b) {
// Sort languages by name
if ($a['name'] === $b['name']) {
return 0;
}
return $a['name'] < $b['name'] ? -1 : 1;
});
return $languages;
}
/**
* Create a list of loaded extensions and their language packs details
*
* @return array
*/
public function getExtensionLanguagePackDetails(): array
{
$activeLanguages = $this->getActiveLanguages();
$packageManager = GeneralUtility::makeInstance(PackageManager::class);
$activePackages = $packageManager->getActivePackages();
$extensions = [];
$activeExtensions = [];
foreach ($activePackages as $package) {
$path = $package->getPackagePath();
$finder = new Finder();
try {
$files = $finder->files()->in($path . 'Resources/Private/Language/')->name('*.xlf');
if ($files->count() === 0) {
// This extension has no .xlf files
continue;
}
} catch (\InvalidArgumentException $e) {
// Dir does not exist
continue;
}
$key = $package->getPackageKey();
$activeExtensions[] = $key;
$title = $package->getValueFromComposerManifest('description') ?? '';
if (is_file($path . 'ext_emconf.php')) {
$_EXTKEY = $key;
$EM_CONF = [];
include $path . 'ext_emconf.php';
$title = $EM_CONF[$key]['title'] ?? $title;
}
$extension = [
'key' => $key,
'title' => $title,
];
if (!empty(ExtensionManagementUtility::getExtensionIcon($path, false))) {
$extension['icon'] = PathUtility::stripPathSitePrefix(ExtensionManagementUtility::getExtensionIcon($path, true));
}
$extension['packs'] = [];
foreach ($activeLanguages as $iso) {
$isLanguagePackDownloaded = is_dir(Environment::getLabelsPath() . '/' . $iso . '/' . $key . '/');
$lastUpdate = $this->registry->get('languagePacks', $iso . '-' . $key);
$extension['packs'][] = [
'iso' => $iso,
'exists' => $isLanguagePackDownloaded,
'lastUpdate' => $this->getFormattedDate($lastUpdate),
];
}
$extensions[] = $extension;
}
usort($extensions, function ($a, $b) {
// Sort extensions by key
if ($a['key'] === $b['key']) {
return 0;
}
return $a['key'] < $b['key'] ? -1 : 1;
});
return $extensions;
}
/**
* Update main language pack download location if possible.
* Store to registry to be used during language pack update
*
* @return string
*/
public function updateMirrorBaseUrl(): string
{
$repositoryUrl = 'https://repositories.typo3.org/mirrors.xml.gz';
$downloadBaseUrl = false;
try {
$response = $this->requestFactory->request($repositoryUrl);
if ($response->getStatusCode() === 200) {
$xmlContent = @gzdecode($response->getBody()->getContents());
if (!empty($xmlContent['mirror']['host']) && !empty($xmlContent['mirror']['path'])) {
$downloadBaseUrl = 'https://' . $xmlContent['mirror']['host'] . $xmlContent['mirror']['path'];
}
}
} catch (\Exception $e) {
// Catch generic exception, fallback handled below
}
if (empty($downloadBaseUrl)) {
// Hard coded fallback if something went wrong fetching & parsing mirror list
$downloadBaseUrl = self::OLD_LANGUAGE_PACK_URL;
}
$this->registry->set('languagePacks', 'baseUrl', $downloadBaseUrl);
return $downloadBaseUrl;
}
/**
* Download and unpack a single language pack of one extension.
*
* @param string $key Extension key
* @param string $iso Language iso code
* @return string One of 'update', 'new' or 'failed'
* @throws \RuntimeException
*/
public function languagePackDownload(string $key, string $iso): string
{
// Sanitize extension and iso code
$availableLanguages = $this->getAvailableLanguages();
$activeLanguages = $this->getActiveLanguages();
if (!array_key_exists($iso, $availableLanguages) || !in_array($iso, $activeLanguages, true)) {
throw new \RuntimeException('Language iso code ' . (string)$iso . ' not available or active', 1520117054);
}
$packageManager = GeneralUtility::makeInstance(PackageManager::class);
$activePackages = $packageManager->getActivePackages();
$packageActive = false;
foreach ($activePackages as $package) {
if ($package->getPackageKey() === $key) {
$packageActive = true;
break;
}
}
if (!$packageActive) {
throw new \RuntimeException('Extension ' . (string)$key . ' not loaded', 1520117245);
}
$languagePackBaseUrl = $this->registry->get('languagePacks', 'baseUrl');
if (empty($languagePackBaseUrl)) {
throw new \RuntimeException('Language pack baseUrl not found', 1520169691);
}
if (in_array($languagePackBaseUrl, self::OLD_LANGUAGE_PACK_URLS, true)
&& GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('newTranslationServer')) {
$languagePackBaseUrl = self::LANGUAGE_PACK_URL;
}
// Allow to modify the base url on the fly by calling a signal
$signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
$signalSlotDispatcher->dispatch(
'TYPO3\\CMS\\Lang\\Service\\TranslationService',
'postProcessMirrorUrl',
[
'extensionKey' => $key,
'mirrorUrl' => &$languagePackBaseUrl,
]
);
$path = ExtensionManagementUtility::extPath($key);
$majorVersion = explode('.', TYPO3_branch)[0];
if (strpos($path, '/sysext/') !== false) {
// This is a system extension and the package URL should be adapted to have different packs per core major version
// https://extensions.typo3.org/fileadmin/l10n/b/a/backend-l10n/backend-l10n-fr.v9.zip
$packageUrl = $key[0] . '/' . $key[1] . '/' . $key . '-l10n/' . $key . '-l10n-' . $iso . '.v' . $majorVersion . '.zip';
} else {
// Typical non sysext path, Hungarian:
// https://extensions.typo3.org/fileadmin/l10n/a/n/anextension-l10n/anextension-l10n-hu.zip
$packageUrl = $key[0] . '/' . $key[1] . '/' . $key . '-l10n/' . $key . '-l10n-' . $iso . '.zip';
}
$absoluteLanguagePath = Environment::getLabelsPath() . '/' . $iso . '/';
$absoluteExtractionPath = $absoluteLanguagePath . $key . '/';
$absolutePathToZipFile = Environment::getVarPath() . '/transient/' . $key . '-l10n-' . $iso . '.zip';
$packExists = is_dir($absoluteExtractionPath);
$packResult = $packExists ? 'update' : 'new';
$operationResult = false;
try {
$response = $this->requestFactory->request($languagePackBaseUrl . $packageUrl);
if ($response->getStatusCode() === 200) {
$languagePackContent = $response->getBody()->getContents();
if (!empty($languagePackContent)) {
$operationResult = true;
if ($packExists) {
$operationResult = GeneralUtility::rmdir($absoluteExtractionPath, true);
}
if ($operationResult) {
GeneralUtility::mkdir_deep(Environment::getVarPath() . '/transient/');
$operationResult = GeneralUtility::writeFileToTypo3tempDir($absolutePathToZipFile, $languagePackContent) === null;
}
$this->unzipTranslationFile($absolutePathToZipFile, $absoluteLanguagePath);
if ($operationResult) {
$operationResult = unlink($absolutePathToZipFile);
}
}
}
} catch (\Exception $e) {
$operationResult = false;
}
if (!$operationResult) {
$packResult = 'failed';
$this->registry->set('languagePacks', $iso . '-' . $key, time());
}
return $packResult;
}
/**
* Set 'last update' timestamp in registry for a series of iso codes.
*
* @param string[] $isos List of iso code timestamps to set
* @throws \RuntimeException
*/
public function setLastUpdatedIsoCode(array $isos)
{
$activeLanguages = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] ?? [];
$registry = GeneralUtility::makeInstance(Registry::class);
foreach ($isos as $iso) {
if (!in_array($iso, $activeLanguages, true)) {
throw new \RuntimeException('Language iso code ' . (string)$iso . ' not available or active', 1520176318);
}
$registry->set('languagePacks', $iso, time());
}
}
/**
* Format a timestamp to a formatted date string
*
* @param int|null $timestamp
* @return string|null
*/
protected function getFormattedDate($timestamp)
{
if (is_int($timestamp)) {
$date = new \DateTime('@' . $timestamp);
$format = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
$timestamp = $date->format($format);
}
return $timestamp;
}
/**
* Unzip an language zip file
*
* @param string $file path to zip file
* @param string $path path to extract to
*/
protected function unzipTranslationFile(string $file, string $path)
{
if (!is_dir($path)) {
GeneralUtility::mkdir_deep($path);
}
$zipService = GeneralUtility::makeInstance(ZipService::class);
if ($zipService->verify($file)) {
$zipService->extract($file, $path);
}
GeneralUtility::fixPermissions($path, true);
}
}
@@ -0,0 +1,117 @@
<?php
namespace TYPO3\CMS\Install\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\Category\CategoryRegistry;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Service for loading the TCA
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class LoadTcaService
{
/**
* Load TCA
* Mostly a copy of ExtensionManagementUtility to include TCA without migrations.
* To be used in install tool only.
*
* This will set up $GLOBALS['TCA']
*
* @see \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::buildBaseTcaFromSingleFiles
*/
public function loadExtensionTablesWithoutMigration()
{
$GLOBALS['TCA'] = [];
$activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
// First load "full table" files from Configuration/TCA
foreach ($activePackages as $package) {
$tcaConfigurationDirectory = $package->getPackagePath() . 'Configuration/TCA';
if (is_dir($tcaConfigurationDirectory)) {
$files = scandir($tcaConfigurationDirectory);
foreach ($files as $file) {
if (is_file($tcaConfigurationDirectory . '/' . $file)
&& ($file !== '.')
&& ($file !== '..')
&& (substr($file, -4, 4) === '.php')
) {
$tcaOfTable = require $tcaConfigurationDirectory . '/' . $file;
if (is_array($tcaOfTable)) {
// TCA table name is filename without .php suffix, eg 'sys_notes', not 'sys_notes.php'
$tcaTableName = substr($file, 0, -4);
$GLOBALS['TCA'][$tcaTableName] = $tcaOfTable;
}
}
}
}
}
// Apply category stuff
CategoryRegistry::getInstance()->applyTcaForPreRegisteredTables();
// Execute override files from Configuration/TCA/Overrides
foreach ($activePackages as $package) {
$tcaOverridesPathForPackage = $package->getPackagePath() . 'Configuration/TCA/Overrides';
if (is_dir($tcaOverridesPathForPackage)) {
$files = scandir($tcaOverridesPathForPackage);
foreach ($files as $file) {
if (is_file($tcaOverridesPathForPackage . '/' . $file)
&& ($file !== '.')
&& ($file !== '..')
&& (substr($file, -4, 4) === '.php')
) {
require $tcaOverridesPathForPackage . '/' . $file;
}
}
}
}
}
/**
* Load ext_tables.php of a single extension
*
* @param string $extensionKey The extension to load a ext_tables.php file from.
*/
public function loadSingleExtTablesFile(string $extensionKey)
{
global $T3_SERVICES, $T3_VAR, $TYPO3_CONF_VARS;
global $TBE_MODULES, $TBE_MODULES_EXT, $TCA;
global $PAGES_TYPES, $TBE_STYLES;
global $_EXTKEY;
$packageManager = GeneralUtility::makeInstance(PackageManager::class);
try {
$package = $packageManager->getPackage($extensionKey);
} catch (\TYPO3\CMS\Core\Package\Exception\UnknownPackageException $e) {
throw new \RuntimeException(
'Extension ' . $extensionKey . ' is not active',
1477217619
);
}
$extTablesPath = $package->getPackagePath() . 'ext_tables.php';
// Load ext_tables.php file of the extension
if (@file_exists($extTablesPath)) {
// $_EXTKEY and $_EXTCONF are available in ext_tables.php
// and are explicitly set in cached file as well
$_EXTKEY = $extensionKey;
$_EXTCONF = $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$_EXTKEY] ?? null;
require $extTablesPath;
}
}
}
@@ -0,0 +1,235 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\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\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Service handling bulk read and write of LocalConfiguration values.
*
* Used by "Configure global settings" / "All configuration" view.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class LocalConfigurationValueService
{
/**
* Get up configuration data. Prepares main TYPO3_CONF_VARS
* array to be displayed and merges is with the description file
*
* @return array Configuration data
*/
public function getCurrentConfigurationData(): array
{
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$localConfiguration = $configurationManager->getMergedLocalConfiguration();
$data = [];
$commentArray = $this->getDefaultConfigArrayComments();
foreach ($localConfiguration as $sectionName => $section) {
if (isset($commentArray[$sectionName])) {
$data[$sectionName]['description'] = $commentArray[$sectionName]['description'] ?? $sectionName;
$data[$sectionName]['items'] = $this->recursiveConfigurationFetching(
$section,
$GLOBALS['TYPO3_CONF_VARS'][$sectionName] ?? null,
$commentArray[$sectionName]
);
}
}
ksort($data);
return $data;
}
/**
* Because configuration entries can be at any sub-array level, we need
* to check entries recursively.
*
* @param array $sections
* @param array|null $sectionsFromCurrentConfiguration
* @param array $descriptions
* @param array $path
* @return array
*/
protected function recursiveConfigurationFetching(array $sections, array $sectionsFromCurrentConfiguration, array $descriptions, array $path = []): array
{
$data = [];
foreach ($sections as $key => $value) {
if (!isset($descriptions['items'][$key])) {
// @todo should we do something here?
continue;
}
$descriptionInfo = $descriptions['items'][$key];
$descriptionType = $descriptionInfo['type'];
$newPath = $path;
$newPath[] = $key;
if ($descriptionType === 'container') {
$valueFromCurrentConfiguration = $sectionsFromCurrentConfiguration[$key] ?? null;
$data = array_merge($data, $this->recursiveConfigurationFetching($value, $valueFromCurrentConfiguration, $descriptionInfo, $newPath));
} elseif (!preg_match('/[' . LF . CR . ']/', (string)$value) || $descriptionType === 'multiline') {
$itemData = [];
$itemData['key'] = implode('/', $newPath);
$itemData['path'] = '[' . implode('][', $newPath) . ']';
$itemData['fieldType'] = $descriptionInfo['type'];
$itemData['description'] = $descriptionInfo['description'];
$itemData['allowedValues'] = $descriptionInfo['allowedValues'];
$itemData['differentValueInCurrentConfiguration'] = (!isset($descriptionInfo['compareValuesWithCurrentConfiguration']) ||
$descriptionInfo['compareValuesWithCurrentConfiguration']) &&
isset($sectionsFromCurrentConfiguration[$key]) &&
$value !== $sectionsFromCurrentConfiguration[$key];
switch ($descriptionType) {
case 'multiline':
$itemData['type'] = 'textarea';
$itemData['value'] = str_replace(['\' . LF . \'', '\' . LF . \''], [LF, LF], $value);
break;
case 'bool':
$itemData['type'] = 'checkbox';
$itemData['value'] = $value ? '1' : '0';
$itemData['checked'] = (bool)$value;
break;
case 'int':
$itemData['type'] = 'number';
$itemData['value'] = (int)$value;
break;
case 'array':
$itemData['type'] = 'input';
// @todo The line below should be improved when the array handling is introduced in the global settings manager.
$itemData['value'] = is_array($value)
? implode(',', $value)
: (string)$value;
break;
// Check if the setting is a PHP error code, will trigger a view helper in fluid
case 'errors':
$itemData['type'] = 'input';
$itemData['value'] = $value;
$itemData['phpErrorCode'] = true;
break;
case 'password':
$itemData['type'] = 'password';
$itemData['value'] = $value;
$itemData['hideValue'] = true;
break;
default:
$itemData['type'] = 'input';
$itemData['value'] = $value;
}
$data[] = $itemData;
}
}
return $data;
}
/**
* Store changed values in LocalConfiguration
*
* @param array $valueList Nested array with key['key'] value
* @return FlashMessageQueue
*/
public function updateLocalConfigurationValues(array $valueList): FlashMessageQueue
{
$messageQueue = new FlashMessageQueue('install');
$configurationPathValuePairs = [];
$commentArray = $this->getDefaultConfigArrayComments();
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
foreach ($valueList as $path => $value) {
$oldValue = $configurationManager->getConfigurationValueByPath($path);
$pathParts = explode('/', $path);
$descriptionData = $commentArray[$pathParts[0]];
while ($part = next($pathParts)) {
$descriptionData = $descriptionData['items'][$part];
}
$dataType = $descriptionData['type'];
if ($dataType === 'multiline') {
// Force Unix line breaks in text areas
// Preserve line breaks
$value = str_replace([CR, LF], ['', '\' . LF . \''], $value);
}
if ($dataType === 'bool') {
// When submitting settings in the Install Tool, values that default to "FALSE" or "TRUE"
// in EXT:core/Configuration/DefaultConfiguration.php will be sent as "0" resp. "1".
$value = $value === '1';
$valueHasChanged = (bool)$oldValue !== $value;
} elseif ($dataType === 'int') {
// Cast integer values to integers (but only for values that can not contain a string as well)
$value = (int)$value;
$valueHasChanged = (int)$oldValue !== $value;
} elseif ($dataType === 'array') {
$oldValueAsString = is_array($oldValue)
? implode(',', $oldValue)
: (string)$oldValue;
$valueHasChanged = $oldValueAsString !== $value;
$value = GeneralUtility::trimExplode(',', $value, true);
} else {
$valueHasChanged = (string)$oldValue !== (string)$value;
}
// Save if value changed
if ($valueHasChanged) {
$configurationPathValuePairs[$path] = $value;
if (is_bool($value)) {
$messageBody = 'New value = ' . ($value ? 'true' : 'false');
} elseif (empty($value)) {
$messageBody = 'New value = none';
} elseif (is_array($value)) {
$messageBody = "New value = ['" . implode("', '", $value) . "']";
} elseif ($dataType === 'password') {
$messageBody = 'New value is set';
} else {
$messageBody = 'New value = ' . $value;
}
$messageQueue->enqueue(new FlashMessage(
$messageBody,
$path
));
}
}
if (!empty($messageQueue)) {
$configurationManager->setLocalConfigurationValuesByPathValuePairs($configurationPathValuePairs);
}
return $messageQueue;
}
/**
* Read descriptions from description file
*
* @return array
*/
protected function getDefaultConfigArrayComments(): array
{
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$fileName = $configurationManager->getDefaultConfigurationDescriptionFileLocation();
$fileLoader = GeneralUtility::makeInstance(YamlFileLoader::class);
return $fileLoader->load($fileName);
}
}
@@ -0,0 +1,564 @@
<?php
namespace TYPO3\CMS\Install\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 Symfony\Component\HttpFoundation\Cookie;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Http\CookieHeaderTrait;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Security\BlockSerializationTrait;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Secure session handling for the install tool.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class SessionService implements SingletonInterface
{
use BlockSerializationTrait;
use CookieHeaderTrait;
/**
* The path to our var/ folder (where we can write our sessions). Set in the
* constructor.
*
* @var string
*/
private $basePath;
/**
* Path where to store our session files in var/session/. %s will be
* non-guessable.
*
* @var string
*/
private $sessionPath = 'session/%s';
/**
* the cookie to store the session ID of the install tool
*
* @var string
*/
private $cookieName = 'Typo3InstallTool';
/**
* time (minutes) to expire an unused session
*
* @var int
*/
private $expireTimeInMinutes = 15;
/**
* time (minutes) to generate a new session id for our current session
*
* @var int
*/
private $regenerateSessionIdTime = 5;
/**
* Constructor. Starts PHP session handling in our own private store
*
* Side-effect: might set a cookie, so must be called before any other output.
*/
public function __construct()
{
$this->basePath = Environment::getVarPath() . '/';
// Start our PHP session early so that hasSession() works
$sessionSavePath = $this->getSessionSavePath();
// Register our "save" session handler
session_set_save_handler([$this, 'open'], [$this, 'close'], [$this, 'read'], [$this, 'write'], [$this, 'destroy'], [$this, 'gc']);
session_save_path($sessionSavePath);
session_name($this->cookieName);
ini_set('session.cookie_httponly', true);
if ($this->hasSameSiteCookieSupport()) {
ini_set('session.cookie_samesite', Cookie::SAMESITE_STRICT);
}
ini_set('session.cookie_path', (string)GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'));
// Always call the garbage collector to clean up stale session files
ini_set('session.gc_probability', (string)100);
ini_set('session.gc_divisor', (string)100);
ini_set('session.gc_maxlifetime', (string)$this->expireTimeInMinutes * 2 * 60);
if ($this->isSessionAutoStartEnabled()) {
$sessionCreationError = 'Error: session.auto-start is enabled.<br />';
$sessionCreationError .= 'The PHP option session.auto-start is enabled. Disable this option in php.ini or .htaccess:<br />';
$sessionCreationError .= '<pre>php_value session.auto_start Off</pre>';
throw new \TYPO3\CMS\Install\Exception($sessionCreationError, 1294587485);
}
if (session_status() === PHP_SESSION_ACTIVE) {
$sessionCreationError = 'Session already started by session_start().<br />';
$sessionCreationError .= 'Make sure no installed extension is starting a session in its ext_localconf.php or ext_tables.php.';
throw new \TYPO3\CMS\Install\Exception($sessionCreationError, 1294587486);
}
session_start();
if (!$this->hasSameSiteCookieSupport()) {
$this->resendCookieHeader([$this->cookieName]);
}
}
/**
* Returns the path where to store our session files
*
* @throws \TYPO3\CMS\Install\Exception
* @return string Session save path
*/
private function getSessionSavePath()
{
if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
throw new \TYPO3\CMS\Install\Exception(
'No encryption key set to secure session',
1371243449
);
}
$sessionSavePath = sprintf(
$this->basePath . $this->sessionPath,
GeneralUtility::hmac('session:' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])
);
$this->ensureSessionSavePathExists($sessionSavePath);
return $sessionSavePath;
}
/**
* Create directories for the session save path
* and throw an exception if that fails.
*
* @param string $sessionSavePath The absolute path to the session files
* @throws \TYPO3\CMS\Install\Exception
*/
private function ensureSessionSavePathExists($sessionSavePath)
{
if (!is_dir($sessionSavePath)) {
try {
GeneralUtility::mkdir_deep($sessionSavePath);
} catch (\RuntimeException $exception) {
throw new \TYPO3\CMS\Install\Exception(
'Could not create session folder in ' . Environment::getVarPath() . '. Make sure it is writeable!',
1294587484
);
}
$htaccessContent = '
# Apache < 2.3
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
Satisfy All
</IfModule>
# Apache ≥ 2.3
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
';
GeneralUtility::writeFile($sessionSavePath . '/.htaccess', $htaccessContent);
$indexContent = '<!DOCTYPE html>';
$indexContent .= '<html><head><title></title><meta http-equiv=Refresh Content="0; Url=../../"/>';
$indexContent .= '</head></html>';
GeneralUtility::writeFile($sessionSavePath . '/index.html', $indexContent);
}
}
/**
* Starts a new session
*
* @return string The session ID
*/
public function startSession()
{
$_SESSION['active'] = true;
// Be sure to use our own session id, so create a new one
return $this->renewSession();
}
/**
* Destroys a session
*/
public function destroySession()
{
session_destroy();
}
/**
* Reset session. Sets _SESSION to empty array.
*/
public function resetSession()
{
$_SESSION = [];
$_SESSION['active'] = false;
}
/**
* Generates a new session ID and sends it to the client.
*
* @return string the new session ID
*/
private function renewSession()
{
session_regenerate_id();
if (!$this->hasSameSiteCookieSupport()) {
$this->resendCookieHeader([$this->cookieName]);
}
return session_id();
}
/**
* Checks whether we already have an active session.
*
* @return bool TRUE if there is an active session, FALSE otherwise
*/
public function hasSession()
{
return $_SESSION['active'] === true;
}
/**
* Returns the session ID of the running session.
*
* @return string the session ID
*/
public function getSessionId()
{
return session_id();
}
/**
* Returns a session hash, which can only be calculated by the server.
* Used to store our session files without exposing the session ID.
*
* @param string $sessionId An alternative session ID. Defaults to our current session ID
* @throws \TYPO3\CMS\Install\Exception
* @return string the session hash
*/
private function getSessionHash($sessionId = '')
{
if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
throw new \TYPO3\CMS\Install\Exception(
'No encryption key set to secure session',
1371243450
);
}
if (!$sessionId) {
$sessionId = $this->getSessionId();
}
return md5($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . '|' . $sessionId);
}
/**
* Marks this session as an "authorized" one (login successful).
* Should only be called if:
* a) we have a valid session running
* b) the "password" or some other authorization mechanism really matched
*/
public function setAuthorized()
{
$_SESSION['authorized'] = true;
$_SESSION['lastSessionId'] = time();
$_SESSION['tstamp'] = time();
$_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
// Renew the session id to avoid session fixation
$this->renewSession();
}
/**
* Marks this session as an "authorized by backend user" one.
* This is called by BackendModuleController from backend context.
*/
public function setAuthorizedBackendSession()
{
$_SESSION['authorized'] = true;
$_SESSION['lastSessionId'] = time();
$_SESSION['tstamp'] = time();
$_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
$_SESSION['isBackendSession'] = true;
// Renew the session id to avoid session fixation
$this->renewSession();
}
/**
* Check if we have an already authorized session
*
* @return bool TRUE if this session has been authorized before (by a correct password)
*/
public function isAuthorized()
{
if (!$_SESSION['authorized']) {
return false;
}
if ($_SESSION['expires'] < time()) {
// This session has already expired
return false;
}
return true;
}
/**
* Check if we have an authorized session from a system maintainer
*
* @return bool TRUE if this session has been authorized before and initialized by a backend system maintainer
*/
public function isAuthorizedBackendUserSession()
{
if (!$_SESSION['authorized'] || !$_SESSION['isBackendSession']) {
return false;
}
if ($_SESSION['expires'] < time()) {
// This session has already expired
return false;
}
return true;
}
/**
* Check if our session is expired.
* Useful only right after a FALSE "isAuthorized" to see if this is the
* reason for not being authorized anymore.
*
* @return bool TRUE if an authorized session exists, but is expired
*/
public function isExpired()
{
if (!$_SESSION['authorized']) {
// Session never existed, means it is not "expired"
return false;
}
if ($_SESSION['expires'] < time()) {
// This session was authorized before, but has expired
return true;
}
return false;
}
/**
* Refreshes our session information, rising the expire time.
* Also generates a new session ID every 5 minutes to minimize the risk of
* session hijacking.
*/
public function refreshSession()
{
$_SESSION['tstamp'] = time();
$_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
if (time() > $_SESSION['lastSessionId'] + $this->regenerateSessionIdTime * 60) {
// Renew our session ID
$_SESSION['lastSessionId'] = time();
$this->renewSession();
}
}
/**
* Add a message to "Flash" message storage.
*
* @param FlashMessage $message A message to add
*/
public function addMessage(FlashMessage $message)
{
if (!is_array($_SESSION['messages'])) {
$_SESSION['messages'] = [];
}
$_SESSION['messages'][] = $message;
}
/**
* Return stored session messages and flush.
*
* @return FlashMessage[] Messages
*/
public function getMessagesAndFlush()
{
$messages = [];
if (is_array($_SESSION['messages'])) {
$messages = $_SESSION['messages'];
}
$_SESSION['messages'] = [];
return $messages;
}
/*************************
*
* PHP session handling with "secure" session files (hashed session id)
* see http://www.php.net/manual/en/function.session-set-save-handler.php
*
*************************/
/**
* Returns the file where to store our session data
*
* @param string $id
* @return string A filename
*/
private function getSessionFile($id)
{
$sessionSavePath = $this->getSessionSavePath();
return $sessionSavePath . '/hash_' . $this->getSessionHash($id);
}
/**
* Open function. See @session_set_save_handler
*
* @param string $savePath
* @param string $sessionName
* @return bool
*/
public function open($savePath, $sessionName)
{
return true;
}
/**
* Close function. See @session_set_save_handler
*
* @return bool
*/
public function close()
{
return true;
}
/**
* Read session data. See @session_set_save_handler
*
* @param string $id The session id
* @return string
*/
public function read($id)
{
$sessionFile = $this->getSessionFile($id);
$content = '';
if (file_exists($sessionFile)) {
if ($fd = fopen($sessionFile, 'rb')) {
$lockres = flock($fd, LOCK_SH);
if ($lockres) {
$length = filesize($sessionFile);
if ($length > 0) {
$content = fread($fd, $length);
}
flock($fd, LOCK_UN);
}
fclose($fd);
}
}
// Do a "test write" of the session file after opening it. The real session data is written in
// __destruct() and we can not create a sane error message there anymore, so this test should fail
// before if final session file can not be written due to permission problems.
$this->write($id, $content);
return $content;
}
/**
* Write session data. See @session_set_save_handler
*
* @param string $id The session id
* @param string $sessionData The data to be stored
* @throws Exception
* @return bool
*/
public function write($id, $sessionData)
{
$sessionFile = $this->getSessionFile($id);
$result = false;
$changePermissions = !@is_file($sessionFile);
if ($fd = fopen($sessionFile, 'cb')) {
if (flock($fd, LOCK_EX)) {
ftruncate($fd, 0);
$res = fwrite($fd, $sessionData);
if ($res !== false) {
fflush($fd);
$result = true;
}
flock($fd, LOCK_UN);
}
fclose($fd);
// Change the permissions only if the file has just been created
if ($changePermissions) {
GeneralUtility::fixPermissions($sessionFile);
}
}
if (!$result) {
throw new Exception(
'Session file not writable. Please check permission on ' .
Environment::getVarPath() . '/session and its subdirectories.',
1424355157
);
}
return $result;
}
/**
* Destroys one session. See @session_set_save_handler
*
* @param string $id The session id
* @return string
*/
public function destroy($id)
{
$sessionFile = $this->getSessionFile($id);
return @unlink($sessionFile);
}
/**
* Garbage collect session info. See @session_set_save_handler
*
* @param int $maxLifeTime The setting of session.gc_maxlifetime
* @return bool
*/
public function gc($maxLifeTime)
{
$sessionSavePath = $this->getSessionSavePath();
$files = glob($sessionSavePath . '/hash_*');
if (!is_array($files)) {
return true;
}
foreach ($files as $filename) {
if (filemtime($filename) + $this->expireTimeInMinutes * 60 < time()) {
@unlink($filename);
}
}
return true;
}
/**
* Writes the session data at the end, to overcome a PHP APC bug.
*
* Writes the session data in a proper context that is not affected by the APC bug:
* http://pecl.php.net/bugs/bug.php?id=16721.
*
* This behaviour was introduced in #17511, where self::write() made use of GeneralUtility
* which due to the APC bug throws a "Fatal error: Class 'GeneralUtility' not found"
* (and the session data is not saved). Calling session_write_close() at this point
* seems to be the most easy solution, according to PHP author.
*/
public function __destruct()
{
session_write_close();
}
/**
* Check if php session.auto_start is enabled
*
* @return bool TRUE if session.auto_start is enabled, FALSE if disabled
*/
protected function isSessionAutoStartEnabled()
{
return $this->getIniValueBoolean('session.auto_start');
}
/**
* Cast an on/off php ini value to boolean
*
* @param string $configOption
* @return bool TRUE if the given option is enabled, FALSE if disabled
*/
protected function getIniValueBoolean($configOption)
{
return filter_var(ini_get($configOption), FILTER_VALIDATE_BOOLEAN, [FILTER_REQUIRE_SCALAR, FILTER_NULL_ON_FAILURE]);
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,156 @@
<?php
namespace TYPO3\CMS\Install\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 Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
use TYPO3\CMS\Core\Resource\StorageRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Service class to manage typo3temp/assets and FAL storage
* processed file statistics / cleanup.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class Typo3tempFileService
{
/**
* Returns a list of directory names in typo3temp/assets and their number of files
*
* @return array
*/
public function getDirectoryStatistics(): array
{
return array_merge(
$this->statsFromTypo3temp(),
$this->statsFromStorages()
);
}
/**
* Directory statistics for typo3temp/assets folders with some
* special handling for legacy processed file storage _processed_
*
* @return array
*/
protected function statsFromTypo3temp(): array
{
$stats = [];
$typo3TempAssetsPath = '/typo3temp/assets/';
$basePath = Environment::getPublicPath() . $typo3TempAssetsPath;
if (is_dir($basePath)) {
$dirFinder = new Finder();
$dirsInAssets = $dirFinder->directories()->in($basePath)->depth(0)->sortByName();
foreach ($dirsInAssets as $dirInAssets) {
/** @var SplFileInfo $dirInAssets */
$fileFinder = new Finder();
$fileCount = $fileFinder->files()->in($dirInAssets->getPathname())->count();
$folderName = $dirInAssets->getFilename();
$stat = [
'directory' => $typo3TempAssetsPath . $folderName,
'numberOfFiles' => $fileCount,
];
if ($folderName === '_processed_') {
// The processed file storage for legacy files (eg. TCA type=group internal_type=file)
// gets the storageUid set, so this one can be removed via FAL functionality
$stat['storageUid'] = 0;
}
$stats[] = $stat;
}
}
return $stats;
}
/**
* Directory statistics for configured FAL storages.
*
* @return array
*/
protected function statsFromStorages(): array
{
$stats = [];
$processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
$storages = GeneralUtility::makeInstance(StorageRepository::class)->findAll();
foreach ($storages as $storage) {
if ($storage->isOnline()) {
$storageConfiguration = $storage->getConfiguration();
$storageBasePath = rtrim($storageConfiguration['basePath'], '/');
$processedPath = '/' . $storageBasePath . $storage->getProcessingFolder()->getIdentifier();
$numberOfFiles = $processedFileRepository->countByStorage($storage);
$stats[] = [
'directory' => $processedPath,
'numberOfFiles' => $numberOfFiles,
'storageUid' => $storage->getUid()
];
}
}
return $stats;
}
/**
* Clear processed files. The sys_file_processedfile table is cleared for
* given storage uid and the physical files of local processed storages are deleted.
*
* @return int 0 if all went well, if >0 this number of files that could not be deleted
*/
public function clearProcessedFiles(int $storageUid): int
{
$repository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
return $repository->removeAll($storageUid);
}
/**
* Clears files and folders in a typo3temp/assets/ folder (not _processed_!)
*
* @param string $folderName
* @return bool TRUE if all went well
* @throws \RuntimeException If folder path is not valid
*/
public function clearAssetsFolder(string $folderName)
{
$basePath = Environment::getPublicPath() . $folderName;
if (empty($folderName)
|| !GeneralUtility::isAllowedAbsPath($basePath)
|| strpos($folderName, '/typo3temp/assets/') !== 0
) {
throw new \RuntimeException(
'Path to folder ' . $folderName . ' not allowed.',
1501781453
);
}
if (!is_dir($basePath)) {
throw new \RuntimeException(
'Folder path ' . $basePath . ' does not exist or is no directory.',
1501781454
);
}
// first remove directories
foreach ((new Finder())->directories()->in($basePath)->depth(0) as $directory) {
/** @var SplFileInfo $directory */
GeneralUtility::rmdir($directory->getPathname(), true);
}
// then remove files directly in the main dir
foreach ((new Finder())->files()->in($basePath)->depth(0) as $file) {
/** @var SplFileInfo $file */
$path = $file->getPathname();
@unlink($path);
}
return true;
}
}
@@ -0,0 +1,469 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\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 Doctrine\DBAL\Platforms\MySqlPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Table;
use Symfony\Component\Console\Output\Output;
use Symfony\Component\Console\Output\StreamOutput;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
use TYPO3\CMS\Core\Database\Schema\SqlReader;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Updates\ChattyInterface;
use TYPO3\CMS\Install\Updates\ConfirmableInterface;
use TYPO3\CMS\Install\Updates\RepeatableInterface;
use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
/**
* Service class helping managing upgrade wizards
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class UpgradeWizardsService
{
private $output;
public function __construct()
{
$this->output = new StreamOutput(fopen('php://temp', 'wb'), Output::VERBOSITY_NORMAL, false);
}
/**
* @return array List of wizards marked as done in registry
*/
public function listOfWizardsDone(): array
{
$wizardsDoneInRegistry = [];
$registry = GeneralUtility::makeInstance(Registry::class);
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
if ($registry->get('installUpdate', $className, false)) {
$wizardInstance = GeneralUtility::makeInstance($className);
$wizardsDoneInRegistry[] = [
'class' => $className,
'identifier' => $identifier,
'title' => $wizardInstance->getTitle(),
];
}
}
return $wizardsDoneInRegistry;
}
/**
* @return array List of row updaters marked as done in registry
* @throws \RuntimeException
*/
public function listOfRowUpdatersDone(): array
{
$registry = GeneralUtility::makeInstance(Registry::class);
$rowUpdatersDoneClassNames = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
$rowUpdatersDone = [];
foreach ($rowUpdatersDoneClassNames as $rowUpdaterClassName) {
// Silently skip non existing DatabaseRowsUpdateWizards
if (!class_exists($rowUpdaterClassName)) {
continue;
}
/** @var RowUpdaterInterface $rowUpdater */
$rowUpdater = GeneralUtility::makeInstance($rowUpdaterClassName);
if (!$rowUpdater instanceof RowUpdaterInterface) {
throw new \RuntimeException(
'Row updater must implement RowUpdaterInterface',
1484152906
);
}
$rowUpdatersDone[] = [
'class' => $rowUpdaterClassName,
'identifier' => $rowUpdaterClassName,
'title' => $rowUpdater->getTitle(),
];
}
return $rowUpdatersDone;
}
/**
* Mark one wizard as undone. This can be a "casual" wizard
* or a single "row updater".
*
* @param string $identifier Wizard or RowUpdater identifier
* @return bool True if wizard has been marked as undone
* @throws \RuntimeException
*/
public function markWizardUndone(string $identifier): bool
{
$this->assertIdentifierIsValid($identifier);
$registry = GeneralUtility::makeInstance(Registry::class);
$aWizardHasBeenMarkedUndone = false;
foreach ($this->listOfWizardsDone() as $wizard) {
if ($wizard['identifier'] === $identifier) {
$aWizardHasBeenMarkedUndone = true;
$registry->set('installUpdate', $wizard['class'], 0);
}
}
if (!$aWizardHasBeenMarkedUndone) {
$rowUpdatersDoneList = $this->listOfRowUpdatersDone();
$registryArray = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
foreach ($rowUpdatersDoneList as $rowUpdater) {
if ($rowUpdater['identifier'] === $identifier) {
$aWizardHasBeenMarkedUndone = true;
foreach ($registryArray as $rowUpdaterMarkedAsDonePosition => $rowUpdaterMarkedAsDone) {
if ($rowUpdaterMarkedAsDone === $rowUpdater['class']) {
unset($registryArray[$rowUpdaterMarkedAsDonePosition]);
break;
}
}
$registry->set('installUpdateRows', 'rowUpdatersDone', $registryArray);
}
}
}
return $aWizardHasBeenMarkedUndone;
}
/**
* Get a list of tables, single columns and indexes to add.
*
* @return array Array with possible keys "tables", "columns", "indexes"
*/
public function getBlockingDatabaseAdds(): array
{
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
$databaseDefinitions = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
$schemaMigrator = GeneralUtility::makeInstance(SchemaMigrator::class);
$databaseDifferences = $schemaMigrator->getSchemaDiffs($databaseDefinitions);
$adds = [];
foreach ($databaseDifferences as $schemaDiff) {
foreach ($schemaDiff->newTables as $newTable) {
/** @var Table $newTable */
if (!is_array($adds['tables'])) {
$adds['tables'] = [];
}
$adds['tables'][] = [
'table' => $newTable->getName(),
];
}
foreach ($schemaDiff->changedTables as $changedTable) {
foreach ($changedTable->addedColumns as $addedColumn) {
/** @var Column $addedColumn */
if (!is_array($adds['columns'])) {
$adds['columns'] = [];
}
$adds['columns'][] = [
'table' => $changedTable->name,
'field' => $addedColumn->getName(),
];
}
foreach ($changedTable->addedIndexes as $addedIndex) {
/** $var Index $addedIndex */
if (!is_array($adds['indexes'])) {
$adds['indexes'] = [];
}
$adds['indexes'][] = [
'table' => $changedTable->name,
'index' => $addedIndex->getName(),
];
}
}
}
return $adds;
}
/**
* Add missing tables, indexes and fields to DB.
*/
public function addMissingTablesAndFields(): array
{
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
$databaseDefinitions = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
$schemaMigrator = GeneralUtility::makeInstance(SchemaMigrator::class);
return $schemaMigrator->install($databaseDefinitions, true);
}
/**
* True if DB main charset on mysql is utf8
*
* @return bool True if charset is ok
*/
public function isDatabaseCharsetUtf8(): bool
{
/** @var \TYPO3\CMS\Core\Database\Connection $connection */
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
$isDefaultConnectionMysql = ($connection->getDatabasePlatform() instanceof MySqlPlatform);
if (!$isDefaultConnectionMysql) {
// Not tested on non mysql
$charsetOk = true;
} else {
$queryBuilder = $connection->createQueryBuilder();
$charset = (string)$queryBuilder->select('DEFAULT_CHARACTER_SET_NAME')
->from('information_schema.SCHEMATA')
->where(
$queryBuilder->expr()->eq(
'SCHEMA_NAME',
$queryBuilder->createNamedParameter($connection->getDatabase(), \PDO::PARAM_STR)
)
)
->setMaxResults(1)
->execute()
->fetchColumn();
// check if database charset is utf-8, also allows utf8mb4
$charsetOk = strpos($charset, 'utf8') === 0;
}
return $charsetOk;
}
/**
* Set default connection MySQL database charset to utf8.
* Should be called only *if* default database connection is actually MySQL
*/
public function setDatabaseCharsetUtf8()
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
$sql = 'ALTER DATABASE ' . $connection->quoteIdentifier($connection->getDatabase()) . ' CHARACTER SET utf8';
$connection->exec($sql);
}
/**
* Get list of registered upgrade wizards not marked done.
*
* @return array List of upgrade wizards in correct order with detail information
*/
public function getUpgradeWizardsList(): array
{
$wizards = [];
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $class) {
if ($this->isWizardDone($identifier)) {
continue;
}
/** @var UpgradeWizardInterface $wizardInstance */
$wizardInstance = GeneralUtility::makeInstance($class);
$explanation = '';
// $explanation is changed by reference in Update objects!
$shouldRenderWizard = false;
if ($wizardInstance instanceof UpgradeWizardInterface) {
if ($wizardInstance instanceof ChattyInterface) {
$wizardInstance->setOutput($this->output);
}
$shouldRenderWizard = $wizardInstance->updateNecessary();
$explanation = $wizardInstance->getDescription();
}
$wizards[] = [
'class' => $class,
'identifier' => $identifier,
'title' => $wizardInstance->getTitle(),
'shouldRenderWizard' => $shouldRenderWizard,
'explanation' => $explanation,
];
}
return $wizards;
}
/**
* Execute the "get user input" step of a wizard
*
* @param string $identifier
* @return array
* @throws \RuntimeException
*/
public function getWizardUserInput(string $identifier): array
{
$this->assertIdentifierIsValid($identifier);
$class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
$updateObject = GeneralUtility::makeInstance($class);
$wizardHtml = '';
if (method_exists($updateObject, 'getUserInput')) {
$wizardHtml = $updateObject->getUserInput('install[values][' . htmlspecialchars($identifier) . ']');
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use ConfirmableInterface directly.',
E_USER_DEPRECATED
);
} elseif ($updateObject instanceof UpgradeWizardInterface && $updateObject instanceof ConfirmableInterface) {
$markup = [];
$radioAttributes = [
'type' => 'radio',
'name' => 'install[values][' . $updateObject->getIdentifier() . '][install]',
'value' => 0
];
$markup[] = '<div class="panel panel-danger">';
$markup[] = ' <div class="panel-heading">';
$markup[] = htmlspecialchars($updateObject->getConfirmation()->getTitle());
$markup[] = ' </div>';
$markup[] = ' <div class="panel-body">';
$markup[] = ' <p>' . nl2br(htmlspecialchars($updateObject->getConfirmation()->getMessage())) . '</p>';
$markup[] = ' <div class="btn-group" data-toggle="buttons">';
if (!$updateObject->getConfirmation()->isRequired()) {
$markup[] = ' <label class="btn btn-default active"><input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . ' checked="checked" />' . $updateObject->getConfirmation()->getDeny() . '</label>';
}
$radioAttributes['value'] = 1;
$markup[] = ' <label class="btn btn-default"><input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . ' />' . $updateObject->getConfirmation()->getConfirm() . '</label>';
$markup[] = ' </div>';
$markup[] = ' </div>';
$markup[] = '</div>';
$wizardHtml = implode('', $markup);
}
$result = [
'identifier' => $identifier,
'title' => $updateObject->getTitle(),
'wizardHtml' => $wizardHtml,
];
return $result;
}
/**
* Execute a single update wizard
*
* @param string $identifier
* @return FlashMessageQueue
* @throws \RuntimeException
*/
public function executeWizard(string $identifier): FlashMessageQueue
{
$this->assertIdentifierIsValid($identifier);
$class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
$updateObject = GeneralUtility::makeInstance($class);
if ($updateObject instanceof ChattyInterface) {
$updateObject->setOutput($this->output);
}
$messages = new FlashMessageQueue('install');
// $wizardInputErrorMessage is given as reference to wizard object!
$wizardInputErrorMessage = '';
if (method_exists($updateObject, 'checkUserInput') &&
!$updateObject->checkUserInput($wizardInputErrorMessage)) {
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use ConfirmableInterface.',
E_USER_DEPRECATED
);
$messages->enqueue(
new FlashMessage(
$wizardInputErrorMessage ?: 'Something went wrong!',
'Input parameter broken',
FlashMessage::ERROR
)
);
} else {
if ($updateObject instanceof UpgradeWizardInterface) {
$requestParams = GeneralUtility::_GP('install');
if ($updateObject instanceof ConfirmableInterface) {
// value is set in request but is empty
$isSetButEmpty = isset($requestParams['values'][$updateObject->getIdentifier()]['install'])
&& empty($requestParams['values'][$updateObject->getIdentifier()]['install']);
$checkValue = (int)$requestParams['values'][$updateObject->getIdentifier()]['install'];
if ($checkValue === 1) {
// confirmation = yes, we do the update
$performResult = $updateObject->executeUpdate();
} elseif ($updateObject->getConfirmation()->isRequired()) {
// confirmation = no, but is required, we do *not* the update and fail
$performResult = false;
} elseif ($isSetButEmpty) {
// confirmation = no, but it is *not* required, we do *not* the update, but mark the wizard as done
$this->output->writeln('No changes applied, marking wizard as done.');
// confirmation was set to "no"
$performResult = true;
}
} else {
// confirmation yes or non-confirmable
$performResult = $updateObject->executeUpdate();
}
}
$stream = $this->output->getStream();
rewind($stream);
if ($performResult) {
if ($updateObject instanceof UpgradeWizardInterface && !($updateObject instanceof RepeatableInterface)) {
// mark wizard as done if it's not repeatable and was successful
$this->markWizardAsDone($updateObject->getIdentifier());
}
$messages->enqueue(
new FlashMessage(
stream_get_contents($stream),
'Update successful'
)
);
} else {
$messages->enqueue(
new FlashMessage(
stream_get_contents($stream),
'Update failed!',
FlashMessage::ERROR
)
);
}
}
return $messages;
}
/**
* Marks some wizard as being "seen" so that it not shown again.
* Writes the info in LocalConfiguration.php
*
* @param string $identifier
* @throws \RuntimeException
*/
public function markWizardAsDone(string $identifier): void
{
$this->assertIdentifierIsValid($identifier);
$class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
GeneralUtility::makeInstance(Registry::class)->set('installUpdate', $class, 1);
}
/**
* Checks if this wizard has been "done" before
*
* @param string $identifier
* @return bool TRUE if wizard has been done before, FALSE otherwise
* @throws \RuntimeException
*/
public function isWizardDone(string $identifier): bool
{
$this->assertIdentifierIsValid($identifier);
$class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
return (bool)GeneralUtility::makeInstance(Registry::class)->get('installUpdate', $class, false);
}
/**
* Validate identifier exists in upgrade wizard list
*
* @param string $identifier
* @throws \RuntimeException
*/
protected function assertIdentifierIsValid(string $identifier): void
{
if ($identifier === '' || (!isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier]) && !is_subclass_of($identifier, RowUpdaterInterface::class))) {
throw new \RuntimeException('No valid wizard identifier given', 1502721731);
}
}
}