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,67 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Controller;
/*
* 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 Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
* Controller abstract for shared parts of the install tool
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
*/
class AbstractController
{
/**
* Helper method to initialize a standalone view instance.
*
* @param ServerRequestInterface $request
* @param string $templatePath
* @return StandaloneView
* @internal param string $template
*/
protected function initializeStandaloneView(ServerRequestInterface $request, string $templatePath): StandaloneView
{
$viewRootPath = GeneralUtility::getFileAbsFileName('EXT:install/Resources/Private/');
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->getRequest()->setControllerExtensionName('Install');
$view->setTemplatePathAndFilename($viewRootPath . 'Templates/' . $templatePath);
$view->setLayoutRootPaths([$viewRootPath . 'Layouts/']);
$view->setPartialRootPaths([$viewRootPath . 'Partials/']);
$view->assignMultiple([
'controller' => $request->getQueryParams()['install']['controller'] ?? 'maintenance',
'context' => $request->getQueryParams()['install']['context'] ?? '',
'composerMode' => Environment::isComposerMode(),
]);
return $view;
}
/**
* Some actions like the database analyzer and the upgrade wizards need additional
* bootstrap actions performed.
*
* Those actions can potentially fatal if some old extension is loaded that triggers
* a fatal in ext_localconf or ext_tables code! Use only if really needed.
*/
protected function loadExtLocalconfDatabaseAndExtTables()
{
\TYPO3\CMS\Core\Core\Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false);
\TYPO3\CMS\Core\Core\Bootstrap::unsetReservedGlobalVariables();
\TYPO3\CMS\Core\Core\Bootstrap::loadBaseTca(false);
\TYPO3\CMS\Core\Core\Bootstrap::loadExtTables(false);
}
}
@@ -0,0 +1,336 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Controller;
/*
* 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 Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Install\Service\SessionService;
use TYPO3\CMS\Rsaauth\RsaEncryptionEncoder;
/**
* Backend module controller to the Install Tool. Sets an Install Tool session
* marked as "initialized by a valid system administrator backend user" and
* redirects to the Install Tool entry point.
*
* This is a classic backend module that does not interfere with other code
* within the Install Tool, it can be seen as a facade around Install Tool just
* to embed the Install Tool in backend.
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
*/
class BackendModuleController
{
private const FLAG_CONFIRMATION_REQUEST = 1;
protected const FLAG_INSTALL_TOOL_PASSWORD = 2;
private const ALLOWED_ACTIONS = ['maintenance', 'settings', 'upgrade', 'environment'];
/**
* @var SessionService
*/
protected $sessionService;
/**
* @var UriBuilder
*/
protected $uriBuilder;
public function __construct()
{
$this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
}
/**
* Shows and handles backend user session confirmation ("sudo mode") for
* accessing a particular Install Tool controller (as given in `$targetController`).
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function backendUserConfirmationAction(ServerRequestInterface $request): ResponseInterface
{
$flags = (int)($request->getQueryParams()['flags'] ?? 0);
$targetController = (string)($request->getQueryParams()['targetController'] ?? '');
$targetHash = (string)($request->getQueryParams()['targetHash'] ?? '');
$expectedTargetHash = GeneralUtility::hmac($targetController, BackendModuleController::class);
$flagInstallToolPassword = (bool)($flags & self::FLAG_INSTALL_TOOL_PASSWORD);
$flagInvalidPassword = false;
if (!in_array($targetController, self::ALLOWED_ACTIONS, true)
|| !hash_equals($expectedTargetHash, $targetHash)) {
return new HtmlResponse('', 403);
}
if ($flags & self::FLAG_CONFIRMATION_REQUEST) {
if ($flagInstallToolPassword && $this->verifyInstallToolPassword($request)) {
return $this->setAuthorizedAndRedirect($targetController);
}
if (!$flagInstallToolPassword && $this->verifyBackendUserPassword($request)) {
return $this->setAuthorizedAndRedirect($targetController);
}
$flagInvalidPassword = true;
}
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->getTemplatePaths()->setTemplatePathAndFilename(
ExtensionManagementUtility::extPath(
'install',
'Resources/Private/Templates/BackendModule/BackendUserConfirmation.html'
)
);
$view->assignMultiple([
'flagInvalidPassword' => $flagInvalidPassword,
'flagInstallToolPassword' => $flagInstallToolPassword,
'languageFileReference' => 'LLL:EXT:install/Resources/Private/Language/BackendModule.xlf',
'passwordModeUri' => $this->getBackendUserConfirmationUri([
'targetController' => $targetController,
'targetHash' => $targetHash,
// current flags, unset FLAG_CONFIRMATION_REQUEST, toggle FLAG_INSTALL_TOOL_PASSWORD
'flags' => $flags & ~self::FLAG_CONFIRMATION_REQUEST ^ self::FLAG_INSTALL_TOOL_PASSWORD,
]),
'verifyUri' => $this->getBackendUserConfirmationUri([
'targetController' => $targetController,
'targetHash' => $targetHash,
// current flags, add FLAG_CONFIRMATION_REQUEST
'flags' => $flags | self::FLAG_CONFIRMATION_REQUEST,
]),
'cancelUri' => '',
]);
$this->applyRsaAuthModules();
return new HtmlResponse($view->render());
}
/**
* Initialize session and redirect to "maintenance"
*
* @return ResponseInterface
*/
public function maintenanceAction(): ResponseInterface
{
return $this->getBackendUserConfirmationRedirect('maintenance')
?? $this->setAuthorizedAndRedirect('maintenance');
}
/**
* Initialize session and redirect to "settings"
*
* @return ResponseInterface
*/
public function settingsAction(): ResponseInterface
{
return $this->getBackendUserConfirmationRedirect('settings')
?? $this->setAuthorizedAndRedirect('settings');
}
/**
* Initialize session and redirect to "upgrade"
*
* @return ResponseInterface
*/
public function upgradeAction(): ResponseInterface
{
return $this->getBackendUserConfirmationRedirect('upgrade')
?? $this->setAuthorizedAndRedirect('upgrade');
}
/**
* Initialize session and redirect to "environment"
*
* @return ResponseInterface
*/
public function environmentAction(): ResponseInterface
{
return $this->getBackendUserConfirmationRedirect('environment')
?? $this->setAuthorizedAndRedirect('environment');
}
/**
* Creates redirect response to backend user confirmation (if required).
*
* @param string $targetController
* @return ResponseInterface|null
*/
protected function getBackendUserConfirmationRedirect(string $targetController): ?ResponseInterface
{
if ($this->getSessionService()->isAuthorizedBackendUserSession()) {
return null;
}
if (Environment::getContext()->isDevelopment()) {
return null;
}
$redirectUri = $this->getBackendUserConfirmationUri([
'targetController' => $targetController,
'targetHash' => GeneralUtility::hmac($targetController, BackendModuleController::class),
]);
return new RedirectResponse((string)$redirectUri, 403);
}
protected function getBackendUserConfirmationUri(array $parameters): Uri
{
return $this->uriBuilder->buildUriFromRoute(
'install.backend-user-confirmation',
$parameters
);
}
/**
* Starts / updates the session and redirects to the Install Tool
* with given action.
*
* @param $controller
* @return ResponseInterface
*/
protected function setAuthorizedAndRedirect(string $controller): ResponseInterface
{
$this->getSessionService()->setAuthorizedBackendSession();
$redirectLocation = 'install.php?install[controller]=' . $controller . '&install[context]=backend';
return new RedirectResponse($redirectLocation, 303);
}
/**
* Verifies that provided password matches Install Tool password.
*
* @param ServerRequestInterface $request
* @return bool
*/
protected function verifyInstallToolPassword(ServerRequestInterface $request): bool
{
$parsedBody = $request->getParsedBody();
$password = $parsedBody['confirmationPassword'] ?? null;
$installToolPassword = $GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'] ?? null;
if (!is_string($password) || empty($installToolPassword)) {
return false;
}
try {
$hashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
$hashInstance = $hashFactory->get($installToolPassword, 'BE');
return $hashInstance->checkPassword($password, $installToolPassword);
} catch (InvalidPasswordHashException $exception) {
return false;
}
}
/**
* Verifies that provided password is actually correct for current backend user
* by stepping through authentication chain in `$GLOBALS['BE_USER]`.
*
* @param ServerRequestInterface $request
* @return bool
*/
protected function verifyBackendUserPassword(ServerRequestInterface $request): bool
{
$parsedBody = $request->getParsedBody();
$password = empty($parsedBody['confirmationPasswordInternal'])
? ($parsedBody['confirmationPassword'] ?? null) // default field
: $parsedBody['confirmationPasswordInternal']; // filled e.g. by `ext:rsaauth`
if (!is_string($password)) {
return false;
}
// clone current backend user object to avoid
// possible side effects for the real instance
$backendUser = clone $this->getBackendUser();
$loginData = [
'status' => 'sudo-mode',
'origin' => BackendModuleController::class,
'uname' => $backendUser->user['username'],
'uident' => $password,
];
// currently there is no dedicated API to perform authentication
// that's why this process partially has to be simulated here
$loginData = $backendUser->processLoginData($loginData);
$authInfo = $backendUser->getAuthInfoArray();
$authenticated = false;
/** @var AbstractAuthenticationService $service or any other service (sic!) */
foreach ($this->getAuthServices($backendUser, $loginData, $authInfo) as $service) {
$ret = (int)$service->authUser($backendUser->user);
if ($ret <= 0) {
return false;
}
if ($ret >= 200) {
return true;
}
if ($ret >= 100) {
continue;
}
$authenticated = true;
continue;
}
return $authenticated;
}
/**
* Initializes authentication services to be used in a foreach loop
*
* @param BackendUserAuthentication $backendUser
* @param array $loginData
* @param array $authInfo
* @return \Generator<int, object>
*/
protected function getAuthServices(BackendUserAuthentication $backendUser, array $loginData, array $authInfo): \Generator
{
$serviceChain = [];
$subType = 'authUserBE';
while ($service = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain)) {
$serviceChain[] = $service->getServiceKey();
if (!is_object($service)) {
break;
}
$service->initAuth($subType, $loginData, $authInfo, $backendUser);
yield $service;
}
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
/**
* Install Tool modified sessions meta-data (handler, storage, name) which
* conflicts with existing session that for instance is used for ext:rsaauth.
*
* @return SessionService
*/
protected function getSessionService(): SessionService
{
if ($this->sessionService === null) {
$this->sessionService = new SessionService();
$this->sessionService->startSession();
}
return $this->sessionService;
}
protected function applyRsaAuthModules()
{
if (!ExtensionManagementUtility::isLoaded('rsaauth')) {
return;
}
$rsaEncryptionEncoder = GeneralUtility::makeInstance(RsaEncryptionEncoder::class);
$rsaEncryptionEncoder->enableRsaEncryption(true);
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,82 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Controller;
/*
* 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 Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconRegistry;
use TYPO3\CMS\Core\Type\Icon\IconState;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Controller for icon handling
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
*/
class IconController extends AbstractController
{
/**
* @var IconRegistry
*/
protected $iconRegistry;
/**
* @var IconFactory
*/
protected $iconFactory;
/**
* Set up dependencies
*/
public function __construct()
{
$this->iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
}
/**
* @return ResponseInterface
* @internal
*/
public function getCacheIdentifierAction(): ResponseInterface
{
return new HtmlResponse($this->iconRegistry->getCacheIdentifier());
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
* @internal
*/
public function getIconAction(ServerRequestInterface $request): ResponseInterface
{
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
$requestedIcon = json_decode($parsedBody['icon'] ?? $queryParams['icon'], true);
list($identifier, $size, $overlayIdentifier, $iconState, $alternativeMarkupIdentifier) = $requestedIcon;
if (empty($overlayIdentifier)) {
$overlayIdentifier = null;
}
$iconState = IconState::cast($iconState);
$icon = $this->iconFactory->getIcon($identifier, $size, $overlayIdentifier, $iconState);
return new HtmlResponse($icon->render($alternativeMarkupIdentifier));
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,174 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Controller;
/*
* 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 Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException;
use TYPO3\CMS\Install\Service\SilentConfigurationUpgradeService;
/**
* Layout controller
*
* Renders a first "load the Javascript in <head>" view, and the
* main layout of the install tool in second action.
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
*/
class LayoutController extends AbstractController
{
/**
* The init action renders an HTML response with HTML view having <head> section
* containing resources to main .js routing.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function initAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeStandaloneView($request, 'Layout/Init.html');
$view->assignMultiple([
// time is used as cache bust for js and css resources
'time' => time(),
'siteName' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
]);
return new HtmlResponse(
$view->render(),
200,
[
'Cache-Control' => 'no-cache, must-revalidate',
'Pragma' => 'no-cache'
]
);
}
/**
* Return a json response with the main HTML layout body: Toolbar, main menu and
* doc header in standalone, doc header only in backend context. Silent updaters
* are executed before this main view is loaded.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function mainLayoutAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeStandaloneView($request, 'Layout/MainLayout.html');
return new JsonResponse([
'success' => true,
'html' => $view->render(),
]);
}
/**
* Execute silent configuration update. May be called multiple times until success = true is returned.
*
* @return ResponseInterface success = true if no change has been done
*/
public function executeSilentConfigurationUpdateAction(): ResponseInterface
{
$silentUpdate = new SilentConfigurationUpgradeService();
$success = true;
try {
$silentUpdate->execute();
} catch (ConfigurationChangedException $e) {
$success = false;
}
return new JsonResponse([
'success' => $success,
]);
}
/**
* Legacy ajax call. This silent updater takes care that all extensions configured in LocalConfiguration
* EXT/extConf serialized array are "upmerged" to arrays within EXTENSIONS if this extension does not
* exist in EXTENSIONS yet.
*
* @return ResponseInterface
* @deprecated since TYPO3 v9, will be removed with TYPO3 v10.0
*/
public function executeSilentLegacyExtConfExtensionConfigurationUpdateAction(): ResponseInterface
{
$configurationManager = new ConfigurationManager();
try {
$oldExtConfSettings = $configurationManager->getConfigurationValueByPath('EXT/extConf');
} catch (MissingArrayPathException $e) {
// The old 'extConf' array may not exist anymore, set to empty array if so.
$oldExtConfSettings = [];
}
try {
$newExtensionSettings = $configurationManager->getConfigurationValueByPath('EXTENSIONS');
} catch (MissingArrayPathException $e) {
// New 'EXTENSIONS' array may not exist yet, for instance if just upgrading to v9
$newExtensionSettings = [];
}
foreach ($oldExtConfSettings as $extensionName => $extensionSettings) {
if (!array_key_exists($extensionName, $newExtensionSettings)) {
$unserializedConfiguration = unserialize($extensionSettings, ['allowed_classes' => false]);
if (is_array($unserializedConfiguration)) {
$newExtensionSettings = $this->removeDotsFromArrayKeysRecursive($unserializedConfiguration);
$configurationManager->setLocalConfigurationValueByPath('EXTENSIONS/' . $extensionName, $newExtensionSettings);
}
}
}
return new JsonResponse([
'success' => true,
]);
}
/**
* Synchronize TYPO3_CONF_VARS['EXTENSIONS'] with possibly new defaults from extensions
* ext_conf_template.txt files. This make LocalConfiguration the only source of truth for
* extension configuration and it is always up to date, also if an extension has been
* updated.
*
* @return ResponseInterface
*/
public function executeSilentExtensionConfigurationSynchronizationAction(): ResponseInterface
{
$extensionConfiguration = new ExtensionConfiguration();
$extensionConfiguration->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
return new JsonResponse([
'success' => true,
]);
}
/**
* Helper method for executeSilentLegacyExtConfExtensionConfigurationUpdateAction(). Old EXT/extConf
* settings have dots at the end of array keys if nested arrays were used. The new configuration does
* not use this funny nested representation anymore. The method removes all dots at the end of given
* array keys recursive to do this transition.
*
* @param array $settings
* @return array New settings
* @deprecated since TYPO3 v9, will be removed with TYPO3 v10.0 along with executeSilentLegacyExtConfExtensionConfigurationUpdateAction()
*/
private function removeDotsFromArrayKeysRecursive(array $settings): array
{
$settingsWithoutDots = [];
foreach ($settings as $key => $value) {
if (is_array($value)) {
$settingsWithoutDots[rtrim($key, '.')] = $this->removeDotsFromArrayKeysRecursive($value);
} else {
$settingsWithoutDots[$key] = $value;
}
}
return $settingsWithoutDots;
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Controller;
/*
* 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 Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Install\Service\EnableFileService;
/**
* Login controller
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
*/
class LoginController extends AbstractController
{
/**
* Render the "Create an "enable install tool file" action
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function showEnableInstallToolFileAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeStandaloneView($request, 'Login/ShowEnableInstallToolFile.html');
return new JsonResponse([
'success' => true,
'html' => $view->render(),
]);
}
/**
* Render login view
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function showLoginAction(ServerRequestInterface $request): ResponseInterface
{
$formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
$view = $this->initializeStandaloneView($request, 'Login/ShowLogin.html');
$view->assignMultiple([
'siteName' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
'loginToken' => $formProtection->generateToken('installTool', 'login'),
'installToolEnableFilePermanent' => EnableFileService::isInstallToolEnableFilePermanent(),
]);
return new JsonResponse([
'success' => true,
'html' => $view->render(),
]);
}
}
@@ -0,0 +1,770 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Controller;
/*
* 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 Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Core\ClassLoadingInformation;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
use TYPO3\CMS\Core\Database\Schema\SqlReader;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Localization\Locales;
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\Install\Service\ClearCacheService;
use TYPO3\CMS\Install\Service\ClearTableService;
use TYPO3\CMS\Install\Service\LanguagePackService;
use TYPO3\CMS\Install\Service\Typo3tempFileService;
/**
* Maintenance controller
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
*/
class MaintenanceController extends AbstractController
{
/**
* Main "show the cards" view
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function cardsAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeStandaloneView($request, 'Maintenance/Cards.html');
return new JsonResponse([
'success' => true,
'html' => $view->render(),
]);
}
/**
* Clear cache framework and opcode caches
*
* @return ResponseInterface
*/
public function cacheClearAllAction(): ResponseInterface
{
GeneralUtility::makeInstance(ClearCacheService::class)->clearAll();
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
$messageQueue = (new FlashMessageQueue('install'))->enqueue(
new FlashMessage('Successfully cleared all caches and all available opcode caches.', 'Caches cleared')
);
return new JsonResponse([
'success' => true,
'status' => $messageQueue,
]);
}
/**
* Clear typo3temp files statistics action
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function clearTypo3tempFilesStatsAction(ServerRequestInterface $request): ResponseInterface
{
$this->loadExtLocalconfDatabaseAndExtTables();
$view = $this->initializeStandaloneView($request, 'Maintenance/ClearTypo3tempFiles.html');
$formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
$view->assignMultiple([
'clearTypo3tempFilesToken' => $formProtection->generateToken('installTool', 'clearTypo3tempFiles'),
]);
return new JsonResponse(
[
'success' => true,
'stats' => (new Typo3tempFileService())->getDirectoryStatistics(),
'html' => $view->render(),
]
);
}
/**
* Clear typo3temp/assets or FAL processed Files
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function clearTypo3tempFilesAction(ServerRequestInterface $request): ResponseInterface
{
$this->loadExtLocalconfDatabaseAndExtTables();
$messageQueue = new FlashMessageQueue('install');
$typo3tempFileService = new Typo3tempFileService();
$folder = $request->getParsedBody()['install']['folder'];
// storageUid is an optional post param if FAL storages should be cleaned
$storageUid = $request->getParsedBody()['install']['storageUid'] ?? null;
if ($storageUid === null) {
$typo3tempFileService->clearAssetsFolder($folder);
$messageQueue->enqueue(new FlashMessage('Cleared files in "' . $folder . '" folder'));
} else {
$storageUid = (int)$storageUid;
$failedDeletions = $typo3tempFileService->clearProcessedFiles($storageUid);
if ($failedDeletions) {
$messageQueue->enqueue(new FlashMessage(
'Failed to delete ' . $failedDeletions . ' processed files. See TYPO3 log (by default typo3temp/var/log/typo3_*.log)',
'',
FlashMessage::ERROR
));
} else {
$messageQueue->enqueue(new FlashMessage('Cleared processed files'));
}
}
return new JsonResponse([
'success' => true,
'status' => $messageQueue,
]);
}
/**
* Dump autoload information
*
* @return ResponseInterface
*/
public function dumpAutoloadAction(): ResponseInterface
{
$messageQueue = new FlashMessageQueue('install');
if (Environment::isComposerMode()) {
$messageQueue->enqueue(new FlashMessage(
'Skipped generating additional class loading information in composer mode.',
'',
FlashMessage::NOTICE
));
} else {
ClassLoadingInformation::dumpClassLoadingInformation();
$messageQueue->enqueue(new FlashMessage(
'Successfully dumped class loading information for extensions.'
));
}
return new JsonResponse([
'success' => true,
'status' => $messageQueue,
]);
}
/**
* Get main database analyzer modal HTML
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function databaseAnalyzerAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeStandaloneView($request, 'Maintenance/DatabaseAnalyzer.html');
$formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
$view->assignMultiple([
'databaseAnalyzerExecuteToken' => $formProtection->generateToken('installTool', 'databaseAnalyzerExecute'),
]);
return new JsonResponse([
'success' => true,
'html' => $view->render(),
]);
}
/**
* Analyze current database situation
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function databaseAnalyzerAnalyzeAction(ServerRequestInterface $request): ResponseInterface
{
$this->loadExtLocalconfDatabaseAndExtTables();
$messageQueue = new FlashMessageQueue('install');
$suggestions = [];
try {
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
$sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
$schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
$addCreateChange = $schemaMigrationService->getUpdateSuggestions($sqlStatements);
// Aggregate the per-connection statements into one flat array
$addCreateChange = array_merge_recursive(...array_values($addCreateChange));
if (!empty($addCreateChange['create_table'])) {
$suggestion = [
'key' => 'addTable',
'label' => 'Add tables',
'enabled' => true,
'children' => [],
];
foreach ($addCreateChange['create_table'] as $hash => $statement) {
$suggestion['children'][] = [
'hash' => $hash,
'statement' => $statement,
];
}
$suggestions[] = $suggestion;
}
if (!empty($addCreateChange['add'])) {
$suggestion = [
'key' => 'addField',
'label' => 'Add fields to tables',
'enabled' => true,
'children' => [],
];
foreach ($addCreateChange['add'] as $hash => $statement) {
$suggestion['children'][] = [
'hash' => $hash,
'statement' => $statement,
];
}
$suggestions[] = $suggestion;
}
if (!empty($addCreateChange['change'])) {
$suggestion = [
'key' => 'change',
'label' => 'Change fields',
'enabled' => false,
'children' => [],
];
foreach ($addCreateChange['change'] as $hash => $statement) {
$child = [
'hash' => $hash,
'statement' => $statement,
];
if (isset($addCreateChange['change_currentValue'][$hash])) {
$child['current'] = $addCreateChange['change_currentValue'][$hash];
}
$suggestion['children'][] = $child;
}
$suggestions[] = $suggestion;
}
// Difference from current to expected
$dropRename = $schemaMigrationService->getUpdateSuggestions($sqlStatements, true);
// Aggregate the per-connection statements into one flat array
$dropRename = array_merge_recursive(...array_values($dropRename));
if (!empty($dropRename['change_table'])) {
$suggestion = [
'key' => 'renameTableToUnused',
'label' => 'Remove tables (rename with prefix)',
'enabled' => false,
'children' => [],
];
foreach ($dropRename['change_table'] as $hash => $statement) {
$child = [
'hash' => $hash,
'statement' => $statement,
];
if (!empty($dropRename['tables_count'][$hash])) {
$child['rowCount'] = $dropRename['tables_count'][$hash];
}
$suggestion['children'][] = $child;
}
$suggestions[] = $suggestion;
}
if (!empty($dropRename['change'])) {
$suggestion = [
'key' => 'renameTableFieldToUnused',
'label' => 'Remove unused fields (rename with prefix)',
'enabled' => false,
'children' => [],
];
foreach ($dropRename['change'] as $hash => $statement) {
$suggestion['children'][] = [
'hash' => $hash,
'statement' => $statement,
];
}
$suggestions[] = $suggestion;
}
if (!empty($dropRename['drop'])) {
$suggestion = [
'key' => 'deleteField',
'label' => 'Drop fields (really!)',
'enabled' => false,
'children' => [],
];
foreach ($dropRename['drop'] as $hash => $statement) {
$suggestion['children'][] = [
'hash' => $hash,
'statement' => $statement,
];
}
$suggestions[] = $suggestion;
}
if (!empty($dropRename['drop_table'])) {
$suggestion = [
'key' => 'deleteTable',
'label' => 'Drop tables (really!)',
'enabled' => false,
'children' => [],
];
foreach ($dropRename['drop_table'] as $hash => $statement) {
$child = [
'hash' => $hash,
'statement' => $statement,
];
if (!empty($dropRename['tables_count'][$hash])) {
$child['rowCount'] = $dropRename['tables_count'][$hash];
}
$suggestion['children'][] = $child;
}
$suggestions[] = $suggestion;
}
} catch (StatementException $e) {
$messageQueue->enqueue(new FlashMessage(
$e->getMessage(),
'Database analysis failed',
FlashMessage::ERROR
));
}
return new JsonResponse([
'success' => true,
'status' => $messageQueue,
'suggestions' => $suggestions,
]);
}
/**
* Apply selected database changes
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function databaseAnalyzerExecuteAction(ServerRequestInterface $request): ResponseInterface
{
$this->loadExtLocalconfDatabaseAndExtTables();
$messageQueue = new FlashMessageQueue('install');
$selectedHashes = $request->getParsedBody()['install']['hashes'] ?? [];
if (empty($selectedHashes)) {
$messageQueue->enqueue(new FlashMessage(
'',
'No database changes selected',
FlashMessage::WARNING
));
} else {
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
$sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
$schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
$statementHashesToPerform = array_flip($selectedHashes);
$results = $schemaMigrationService->migrate($sqlStatements, $statementHashesToPerform);
// Create error flash messages if any
foreach ($results as $errorMessage) {
$messageQueue->enqueue(new FlashMessage(
'Error: ' . $errorMessage,
'Database update failed',
FlashMessage::ERROR
));
}
$messageQueue->enqueue(new FlashMessage(
'Executed database updates',
'Executed database updates'
));
}
return new JsonResponse([
'success' => true,
'status' => $messageQueue,
]);
}
/**
* Clear table overview statistics action
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function clearTablesStatsAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeStandaloneView($request, 'Maintenance/ClearTables.html');
$formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
$view->assignMultiple([
'clearTablesClearToken' => $formProtection->generateToken('installTool', 'clearTablesClear'),
]);
return new JsonResponse([
'success' => true,
'stats' => (new ClearTableService())->getTableStatistics(),
'html' => $view->render(),
]);
}
/**
* Truncate a specific table
*
* @param ServerRequestInterface $request
* @return ResponseInterface
* @throws \RuntimeException
*/
public function clearTablesClearAction(ServerRequestInterface $request): ResponseInterface
{
$table = $request->getParsedBody()['install']['table'];
if (empty($table)) {
throw new \RuntimeException(
'No table name given',
1501944076
);
}
(new ClearTableService())->clearSelectedTable($table);
$messageQueue = (new FlashMessageQueue('install'))->enqueue(
new FlashMessage('Cleared table')
);
return new JsonResponse([
'success' => true,
'status' => $messageQueue
]);
}
/**
* Create Admin Get Data action
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function createAdminGetDataAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeStandaloneView($request, 'Maintenance/CreateAdmin.html');
$formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
$view->assignMultiple([
'createAdminToken' => $formProtection->generateToken('installTool', 'createAdmin'),
]);
return new JsonResponse([
'success' => true,
'html' => $view->render(),
]);
}
/**
* Create a backend administrator from given username and password
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function createAdminAction(ServerRequestInterface $request): ResponseInterface
{
$username = preg_replace('/\\s/i', '', $request->getParsedBody()['install']['userName']);
$password = $request->getParsedBody()['install']['userPassword'];
$passwordCheck = $request->getParsedBody()['install']['userPasswordCheck'];
$isSystemMaintainer = ((bool)$request->getParsedBody()['install']['userSystemMaintainer'] == '1') ? true : false;
$messages = new FlashMessageQueue('install');
if (strlen($username) < 1) {
$messages->enqueue(new FlashMessage(
'No valid username given.',
'Administrator user not created',
FlashMessage::ERROR
));
} elseif ($password !== $passwordCheck) {
$messages->enqueue(new FlashMessage(
'Passwords do not match.',
'Administrator user not created',
FlashMessage::ERROR
));
} elseif (strlen($password) < 8) {
$messages->enqueue(new FlashMessage(
'Password must be at least eight characters long.',
'Administrator user not created',
FlashMessage::ERROR
));
} else {
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$userExists = $connectionPool->getConnectionForTable('be_users')
->count(
'uid',
'be_users',
['username' => $username]
);
if ($userExists) {
$messages->enqueue(new FlashMessage(
'A user with username "' . $username . '" exists already.',
'Administrator user not created',
FlashMessage::ERROR
));
} else {
$hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE');
$hashedPassword = $hashInstance->getHashedPassword($password);
$adminUserFields = [
'username' => $username,
'password' => $hashedPassword,
'admin' => 1,
'tstamp' => $GLOBALS['EXEC_TIME'],
'crdate' => $GLOBALS['EXEC_TIME']
];
$connectionPool->getConnectionForTable('be_users')->insert('be_users', $adminUserFields);
if ($isSystemMaintainer) {
// Get the new admin user uid juste created
$newAdminUserUid = (int)$connectionPool->getConnectionForTable('be_users')->lastInsertId('be_users');
// Get the list of the existing systemMaintainer
$existingSystemMaintainersList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
// Add the new admin user to the existing systemMaintainer list
$newSystemMaintainersList = $existingSystemMaintainersList;
$newSystemMaintainersList[] = $newAdminUserUid;
// Update the LocalConfiguration.php file with the new list
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$configurationManager->setLocalConfigurationValuesByPathValuePairs(
['SYS/systemMaintainers' => $newSystemMaintainersList]
);
}
$messages->enqueue(new FlashMessage(
'',
'Administrator created with username "' . $username . '".'
));
}
}
return new JsonResponse([
'success' => true,
'status' => $messages,
]);
}
/**
* Entry action of language packs module gets
* * list of available languages with details like active or not and last update
* * list of loaded extensions
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function languagePacksGetDataAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeStandaloneView($request, 'Maintenance/LanguagePacks.html');
$formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
$view->assignMultiple([
'languagePacksActivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksActivateLanguage'),
'languagePacksDeactivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksDeactivateLanguage'),
'languagePacksUpdatePackToken' => $formProtection->generateToken('installTool', 'languagePacksUpdatePack'),
'languagePacksUpdateIsoTimesToken' => $formProtection->generateToken('installTool', 'languagePacksUpdateIsoTimes'),
]);
// This action needs TYPO3_CONF_VARS for full GeneralUtility::getUrl() config
$this->loadExtLocalconfDatabaseAndExtTables();
$languagePacksService = GeneralUtility::makeInstance(LanguagePackService::class);
$languagePacksService->updateMirrorBaseUrl();
$extensions = $languagePacksService->getExtensionLanguagePackDetails();
return new JsonResponse([
'success' => true,
'languages' => $languagePacksService->getLanguageDetails(),
'extensions' => $extensions,
'activeLanguages' => $languagePacksService->getActiveLanguages(),
'activeExtensions' => array_column($extensions, 'key'),
'html' => $view->render(),
]);
}
/**
* Activate a language and any possible dependency it may have
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function languagePacksActivateLanguageAction(ServerRequestInterface $request): ResponseInterface
{
$messageQueue = new FlashMessageQueue('install');
$languagePackService = GeneralUtility::makeInstance(LanguagePackService::class);
$locales = GeneralUtility::makeInstance(Locales::class);
$availableLanguages = $languagePackService->getAvailableLanguages();
$activeLanguages = $languagePackService->getActiveLanguages();
$iso = $request->getParsedBody()['install']['iso'];
$activateArray = [];
foreach ($availableLanguages as $availableIso => $name) {
if ($availableIso === $iso && !in_array($availableIso, $activeLanguages, true)) {
$activateArray[] = $iso;
$dependencies = $locales->getLocaleDependencies($availableIso);
if (!empty($dependencies)) {
foreach ($dependencies as $dependency) {
if (!in_array($dependency, $activeLanguages, true)) {
$activateArray[] = $dependency;
}
}
}
}
}
if (!empty($activateArray)) {
$activeLanguages = array_merge($activeLanguages, $activateArray);
sort($activeLanguages);
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$configurationManager->setLocalConfigurationValueByPath(
'EXTCONF/lang',
['availableLanguages' => $activeLanguages]
);
$activationArray = [];
foreach ($activateArray as $activateIso) {
$activationArray[] = $availableLanguages[$activateIso] . ' (' . $activateIso . ')';
}
$messageQueue->enqueue(
new FlashMessage(
'These languages have been activated: ' . implode(', ', $activationArray)
)
);
} else {
$messageQueue->enqueue(
new FlashMessage('Language with ISO code "' . $iso . '" not found or already active.', '', FlashMessage::ERROR)
);
}
return new JsonResponse([
'success' => true,
'status' => $messageQueue,
]);
}
/**
* Deactivate a language if no other active language depends on it
*
* @param ServerRequestInterface $request
* @return ResponseInterface
* @throws \RuntimeException
*/
public function languagePacksDeactivateLanguageAction(ServerRequestInterface $request): ResponseInterface
{
$messageQueue = new FlashMessageQueue('install');
$languagePackService = GeneralUtility::makeInstance(LanguagePackService::class);
$locales = GeneralUtility::makeInstance(Locales::class);
$availableLanguages = $languagePackService->getAvailableLanguages();
$activeLanguages = $languagePackService->getActiveLanguages();
$iso = $request->getParsedBody()['install']['iso'];
if (empty($iso)) {
throw new \RuntimeException('No iso code given', 1520109807);
}
$otherActiveLanguageDependencies = [];
foreach ($activeLanguages as $activeLanguage) {
if ($activeLanguage === $iso) {
continue;
}
$dependencies = $locales->getLocaleDependencies($activeLanguage);
if (in_array($iso, $dependencies, true)) {
$otherActiveLanguageDependencies[] = $activeLanguage;
}
}
if (!empty($otherActiveLanguageDependencies)) {
// Error: Must disable dependencies first
$dependentArray = [];
foreach ($otherActiveLanguageDependencies as $dependency) {
$dependentArray[] = $availableLanguages[$dependency] . ' (' . $dependency . ')';
}
$messageQueue->enqueue(
new FlashMessage(
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" can not be deactivated. These'
. ' other languages depend on it and need to be deactivated before:'
. implode(', ', $dependentArray),
'',
FlashMessage::ERROR
)
);
} else {
if (in_array($iso, $activeLanguages, true)) {
// Deactivate this language
$newActiveLanguages = [];
foreach ($activeLanguages as $activeLanguage) {
if ($activeLanguage === $iso) {
continue;
}
$newActiveLanguages[] = $activeLanguage;
}
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$configurationManager->setLocalConfigurationValueByPath(
'EXTCONF/lang',
['availableLanguages' => $newActiveLanguages]
);
$messageQueue->enqueue(
new FlashMessage(
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has been deactivated'
)
);
} else {
$messageQueue->enqueue(
new FlashMessage(
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has not been deactivated',
'',
FlashMessage::ERROR
)
);
}
}
return new JsonResponse([
'success' => true,
'status' => $messageQueue,
]);
}
/**
* Update a pack of one extension and one language
*
* @param ServerRequestInterface $request
* @return ResponseInterface
* @throws \RuntimeException
*/
public function languagePacksUpdatePackAction(ServerRequestInterface $request): ResponseInterface
{
$this->loadExtLocalconfDatabaseAndExtTables();
$iso = $request->getParsedBody()['install']['iso'];
$key = $request->getParsedBody()['install']['extension'];
$languagePackService = GeneralUtility::makeInstance(LanguagePackService::class);
return new JsonResponse([
'success' => true,
'packResult' => $languagePackService->languagePackDownload($key, $iso)
]);
}
/**
* Set "last updated" time in registry for fully updated language packs.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function languagePacksUpdateIsoTimesAction(ServerRequestInterface $request): ResponseInterface
{
$isos = $request->getParsedBody()['install']['isos'];
$languagePackService = GeneralUtility::makeInstance(LanguagePackService::class);
$languagePackService->setLastUpdatedIsoCode($isos);
// 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->getCache('l10n')->flush();
return new JsonResponse(['success' => true]);
}
/**
* Set 'uc' field of all backend users to empty string
*
* @return ResponseInterface
*/
public function resetBackendUserUcAction(): ResponseInterface
{
GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('be_users')
->update('be_users')
->set('uc', '')
->execute();
$messageQueue = new FlashMessageQueue('install');
$messageQueue->enqueue(new FlashMessage(
'Preferences of all backend users have been reset',
'Reset preferences of all backend users'
));
return new JsonResponse([
'success' => true,
'status' => $messageQueue
]);
}
}
@@ -0,0 +1,480 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Controller;
/*
* 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 Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Install\Configuration\FeatureManager;
use TYPO3\CMS\Install\Service\ExtensionConfigurationService;
use TYPO3\CMS\Install\Service\LocalConfigurationValueService;
/**
* Settings controller
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
*/
class SettingsController extends AbstractController
{
/**
* Main "show the cards" view
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function cardsAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeStandaloneView($request, 'Settings/Cards.html');
return new JsonResponse([
'success' => true,
'html' => $view->render(),
]);
}
/**
* Change install tool password
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function changeInstallToolPasswordGetDataAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeStandaloneView($request, 'Settings/ChangeInstallToolPassword.html');
$formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
$view->assignMultiple([
'changeInstallToolPasswordToken' => $formProtection->generateToken('installTool', 'changeInstallToolPassword'),
]);
return new JsonResponse([
'success' => true,
'html' => $view->render(),
]);
}
/**
* Change install tool password
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function changeInstallToolPasswordAction(ServerRequestInterface $request): ResponseInterface
{
$password = $request->getParsedBody()['install']['password'] ?? '';
$passwordCheck = $request->getParsedBody()['install']['passwordCheck'];
$messageQueue = new FlashMessageQueue('install');
if ($password !== $passwordCheck) {
$messageQueue->enqueue(new FlashMessage(
'Install tool password not changed. Given passwords do not match.',
'',
FlashMessage::ERROR
));
} elseif (strlen($password) < 8) {
$messageQueue->enqueue(new FlashMessage(
'Install tool password not changed. Given password must be at least eight characters long.',
'',
FlashMessage::ERROR
));
} else {
$hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE');
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$configurationManager->setLocalConfigurationValueByPath(
'BE/installToolPassword',
$hashInstance->getHashedPassword($password)
);
$messageQueue->enqueue(new FlashMessage('Install tool password changed'));
}
return new JsonResponse([
'success' => true,
'status' => $messageQueue,
]);
}
/**
* Return a list of possible and active system maintainers
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function systemMaintainerGetListAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeStandaloneView($request, 'Settings/SystemMaintainer.html');
$formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
$view->assignMultiple([
'systemMaintainerWriteToken' => $formProtection->generateToken('installTool', 'systemMaintainerWrite'),
'systemMaintainerIsDevelopmentContext' => GeneralUtility::getApplicationContext()->isDevelopment(),
]);
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
// We have to respect the enable fields here by our own because no TCA is loaded
$queryBuilder = $connectionPool->getQueryBuilderForTable('be_users');
$queryBuilder->getRestrictions()->removeAll();
$users = $queryBuilder
->select('uid', 'username', 'disable', 'starttime', 'endtime')
->from('be_users')
->where(
$queryBuilder->expr()->andX(
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
$queryBuilder->expr()->neq('username', $queryBuilder->createNamedParameter('_cli_', \PDO::PARAM_STR))
)
)
->orderBy('uid')
->execute()
->fetchAll();
$systemMaintainerList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
$systemMaintainerList = array_map('intval', $systemMaintainerList);
$currentTime = time();
foreach ($users as &$user) {
$user['disable'] = $user['disable'] ||
((int)$user['starttime'] !== 0 && $user['starttime'] > $currentTime) ||
((int)$user['endtime'] !== 0 && $user['endtime'] < $currentTime);
$user['isSystemMaintainer'] = in_array((int)$user['uid'], $systemMaintainerList, true);
}
return new JsonResponse([
'success' => true,
'status' => [],
'users' => $users,
'html' => $view->render(),
]);
}
/**
* Write new system maintainer list
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function systemMaintainerWriteAction(ServerRequestInterface $request): ResponseInterface
{
// Sanitize given user list and write out
$newUserList = [];
$users = $request->getParsedBody()['install']['users'] ?? [];
if (is_array($users)) {
foreach ($users as $uid) {
if (MathUtility::canBeInterpretedAsInteger($uid)) {
$newUserList[] = (int)$uid;
}
}
}
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
$queryBuilder->getRestrictions()->removeAll();
$validatedUserList = $queryBuilder
->select('uid')
->from('be_users')
->where(
$queryBuilder->expr()->andX(
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($newUserList, Connection::PARAM_INT_ARRAY))
)
)->execute()->fetchAll();
$validatedUserList = array_column($validatedUserList, 'uid');
$validatedUserList = array_map('intval', $validatedUserList);
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$configurationManager->setLocalConfigurationValuesByPathValuePairs(
['SYS/systemMaintainers' => $validatedUserList]
);
$messages = [];
if (empty($validatedUserList)) {
$messages[] = new FlashMessage(
'',
'Set system maintainer list to an empty array',
FlashMessage::INFO
);
} else {
$messages[] = new FlashMessage(
implode(', ', $validatedUserList),
'New system maintainer uid list',
FlashMessage::INFO
);
}
return new JsonResponse([
'success' => true,
'status' => $messages
]);
}
/**
* Main LocalConfiguration card content
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function localConfigurationGetContentAction(ServerRequestInterface $request): ResponseInterface
{
$localConfigurationValueService = new LocalConfigurationValueService();
$formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
$view = $this->initializeStandaloneView($request, 'Settings/LocalConfigurationGetContent.html');
$view->assignMultiple([
'localConfigurationWriteToken' => $formProtection->generateToken('installTool', 'localConfigurationWrite'),
'localConfigurationData' => $localConfigurationValueService->getCurrentConfigurationData(),
]);
return new JsonResponse([
'success' => true,
'html' => $view->render(),
]);
}
/**
* Write given LocalConfiguration settings
*
* @param ServerRequestInterface $request
* @return ResponseInterface
* @throws \RuntimeException
*/
public function localConfigurationWriteAction(ServerRequestInterface $request): ResponseInterface
{
$settings = $request->getParsedBody()['install']['configurationValues'];
if (!is_array($settings) || empty($settings)) {
throw new \RuntimeException(
'Expected value array not found',
1502282283
);
}
$localConfigurationValueService = new LocalConfigurationValueService();
$messageQueue = $localConfigurationValueService->updateLocalConfigurationValues($settings);
if (empty($messageQueue)) {
$messageQueue->enqueue(new FlashMessage(
'',
'No values changed',
FlashMessage::WARNING
));
}
return new JsonResponse([
'success' => true,
'status' => $messageQueue,
]);
}
/**
* Main preset card content
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function presetsGetContentAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeStandaloneView($request, 'Settings/PresetsGetContent.html');
$presetFeatures = GeneralUtility::makeInstance(FeatureManager::class);
$presetFeatures = $presetFeatures->getInitializedFeatures($request->getParsedBody()['install']['values'] ?? []);
$formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
$view->assignMultiple([
'presetsActivateToken' => $formProtection->generateToken('installTool', 'presetsActivate'),
// This action is called again from within the card itself if a custom image path is supplied
'presetsGetContentToken' => $formProtection->generateToken('installTool', 'presetsGetContent'),
'presetFeatures' => $presetFeatures,
]);
return new JsonResponse([
'success' => true,
'html' => $view->render(),
]);
}
/**
* Write selected presets
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function presetsActivateAction(ServerRequestInterface $request): ResponseInterface
{
$messages = new FlashMessageQueue('install');
$configurationManager = new ConfigurationManager();
$featureManager = new FeatureManager();
$configurationValues = $featureManager->getConfigurationForSelectedFeaturePresets($request->getParsedBody()['install']['values'] ?? []);
if (!empty($configurationValues)) {
$configurationManager->setLocalConfigurationValuesByPathValuePairs($configurationValues);
$messageBody = [];
foreach ($configurationValues as $configurationKey => $configurationValue) {
$messageBody[] = '\'' . $configurationKey . '\' => \'' . $configurationValue . '\'';
}
$messages->enqueue(new FlashMessage(
implode(', ', $messageBody),
'Configuration written'
));
} else {
$messages->enqueue(new FlashMessage(
'',
'No configuration change selected',
FlashMessage::INFO
));
}
return new JsonResponse([
'success' => true,
'status' => $messages,
]);
}
/**
* Render a list of extensions with their configuration form.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function extensionConfigurationGetContentAction(ServerRequestInterface $request): ResponseInterface
{
// Extension configuration needs initialized $GLOBALS['LANG']
Bootstrap::initializeLanguageObject();
$extensionConfigurationService = new ExtensionConfigurationService();
$extensionsWithConfigurations = [];
$activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
foreach ($activePackages as $extensionKey => $activePackage) {
if (@file_exists($activePackage->getPackagePath() . 'ext_conf_template.txt')) {
$extensionsWithConfigurations[$extensionKey] = [
'packageInfo' => $activePackage,
'configuration' => $extensionConfigurationService->getConfigurationPreparedForView($extensionKey),
];
}
}
ksort($extensionsWithConfigurations);
$formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
$view = $this->initializeStandaloneView($request, 'Settings/ExtensionConfigurationGetContent.html');
$view->assignMultiple([
'extensionsWithConfigurations' => $extensionsWithConfigurations,
'extensionConfigurationWriteToken' => $formProtection->generateToken('installTool', 'extensionConfigurationWrite'),
]);
return new JsonResponse([
'success' => true,
'html' => $view->render(),
]);
}
/**
* Write extension configuration
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function extensionConfigurationWriteAction(ServerRequestInterface $request): ResponseInterface
{
$extensionKey = $request->getParsedBody()['install']['extensionKey'];
$configuration = $request->getParsedBody()['install']['extensionConfiguration'];
$nestedConfiguration = [];
foreach ($configuration as $configKey => $value) {
$nestedConfiguration = ArrayUtility::setValueByPath($nestedConfiguration, $configKey, $value, '.');
}
(new ExtensionConfiguration())->set($extensionKey, '', $nestedConfiguration);
$messages = [
new FlashMessage(
'Successfully saved configuration for extension "' . $extensionKey . '"',
'',
FlashMessage::OK
)
];
return new JsonResponse([
'success' => true,
'status' => $messages,
]);
}
/**
* Render feature toggles
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function featuresGetContentAction(ServerRequestInterface $request): ResponseInterface
{
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$configurationDescription = GeneralUtility::makeInstance(YamlFileLoader::class)
->load($configurationManager->getDefaultConfigurationDescriptionFileLocation());
$allFeatures = $GLOBALS['TYPO3_CONF_VARS']['SYS']['features'] ?? [];
$features = [];
foreach ($allFeatures as $featureName => $featureValue) {
// Only features that have a .yml description will be listed. There is currently no
// way for extensions to extend this, so feature toggles of non-core extensions are
// not listed here.
if (isset($configurationDescription['SYS']['items']['features']['items'][$featureName]['description'])) {
$default = $configurationManager->getDefaultConfigurationValueByPath('SYS/features/' . $featureName);
$features[] = [
'name' => $featureName,
'description' => $configurationDescription['SYS']['items']['features']['items'][$featureName]['description'],
'default' => $default,
'value' => $featureValue,
];
}
}
$formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
$view = $this->initializeStandaloneView($request, 'Settings/FeaturesGetContent.html');
$view->assignMultiple([
'features' => $features,
'featuresSaveToken' => $formProtection->generateToken('installTool', 'featuresSave'),
]);
return new JsonResponse([
'success' => true,
'html' => $view->render(),
]);
}
/**
* Update feature toggles state
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function featuresSaveAction(ServerRequestInterface $request): ResponseInterface
{
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$enabledFeaturesFromPost = $request->getParsedBody()['install']['values'] ?? [];
$allFeatures = array_keys($GLOBALS['TYPO3_CONF_VARS']['SYS']['features'] ?? []);
$configurationDescription = GeneralUtility::makeInstance(YamlFileLoader::class)
->load($configurationManager->getDefaultConfigurationDescriptionFileLocation());
foreach ($allFeatures as $featureName) {
// Only features that have a .yml description will be listed. There is currently no
// way for extensions to extend this, so feature toggles of non-core extensions are
// not considered.
if (isset($configurationDescription['SYS']['items']['features']['items'][$featureName]['description'])) {
if (isset($enabledFeaturesFromPost[$featureName])) {
$configurationManager->enableFeature($featureName);
} else {
$configurationManager->disableFeature($featureName);
}
}
}
$messages = [
new FlashMessage(
'Successfully updated feature toggles',
'',
FlashMessage::OK
)
];
return new JsonResponse([
'success' => true,
'status' => $messages,
]);
}
}
File diff suppressed because it is too large. Load diff