Commit 3896e163 authored by Christian Kuhn's avatar Christian Kuhn Committed by Susanne Moog
Browse files

[TASK] Install tool: JS driven routing

The install tool suffered from three main issues since 6.2 rewrite:
* The "step" installer was re-used for recovery and installation
* The routing logic was server based and threw lots of redirects
  which lead to redirect loops
* The Controller/Action class structure was weird and hard to
  understand

The patch solves this with a rather huge rewrite:
* There are two request handlers: One for the Installer, one for
  the install tool.
* A simple list of controllers. One InstallerController for the
  step installer, the others for the main module points of the
  install tool and a Login and a Layout controller.
* Single action code is moved into controllers.
* Both tool and installer first load only a <head> section that
  contain JS references. All other calls are ajax based and the
  routing is done JS side.
* Installer and install tool no longer share controller code
  or templates, the installer can potentially be fully extracted
  from ext:install in another step.
* Installer.js is the "walk through installation" module of the
  installer.
* Router.js is the routing module for the install tool, all tool
  ajax requests get specific urls from this module and hand over
  errors to the Router.
* Error handling is handled on JS side: If for instance
  the login session expires and user clicks elsewhere, a 403
  response is returned which is handled by JS to route to login.
  This is also the place where a recovery can hook in later.
* The silent configuration updater is executed again which was
  removed during one of the previous master patches.
* The template structure is much easier.
* Various card content which has been calculated when loading
  the card layout is moved to the ajax code for single card
  content. That increases the performance of the main module
  points and makes them pretty snappy.

Change-Id: Ib40f40acba17bb47142c0da1bcfb389ab9b4b3a1
Resolves: #82504
Releases: master
Reviewed-on: https://review.typo3.org/54128


Tested-by: default avatarStefan Neufeind <typo3.neufeind@speedpartner.de>
Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Mona Muzaffar's avatarMona Muzaffar <mona.muzaffar@gmx.de>
Tested-by: Mona Muzaffar's avatarMona Muzaffar <mona.muzaffar@gmx.de>
Tested-by: Susanne Moog's avatarSusanne Moog <susanne.moog@typo3.org>
Reviewed-by: Susanne Moog's avatarSusanne Moog <susanne.moog@typo3.org>
parent ea160516
......@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Core\Configuration;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Crypto\Random;
use TYPO3\CMS\Core\Service\OpcodeCacheService;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -399,6 +400,9 @@ class ConfigurationManager
$additionalFactoryConfigurationArray
);
}
$randomKey = GeneralUtility::makeInstance(Random::class)->generateRandomHexString(96);
$localConfigurationArray['SYS']['encryptionKey'] = $randomKey;
$this->writeLocalConfiguration($localConfigurationArray);
}
......
......@@ -303,8 +303,6 @@ return [
'record' => \TYPO3\CMS\Core\LinkHandling\RecordLinkHandler::class,
],
'livesearch' => [], // Array: keywords used for commands to search for specific tables
'isInitialInstallationInProgress' => false,
'isInitialDatabaseImportDone' => true,
'formEngine' => [
'nodeRegistry' => [], // Array: Registry to add or overwrite FormEngine nodes. Main key is a timestamp of the date when an entry is added, sub keys type, priority and class are required. Class must implement TYPO3\CMS\Backend\Form\NodeInterface.
'nodeResolver' => [], // Array: Additional node resolver. Main key is a timestamp of the date when an entry is added, sub keys type, priority and class are required. Class must implement TYPO3\CMS\Backend\Form\NodeResolverInterface.
......
......@@ -215,12 +215,6 @@ SYS:
generateApacheHtaccess:
type: bool
description: 'TYPO3 can create <em>.htaccess</em> files which are used by Apache Webserver. They are useful for access protection or performance improvements. Currently <em>.htaccess</em> files in the following directories are created, if they do not exist: <ul><li>typo3temp/compressor/</li></ul>You want to disable this feature, if you are not running Apache or want to use own rulesets.'
isInitialInstallationInProgress:
type: bool
description: 'If TRUE, the installation is ''in progress''. This value is handled within the install tool step installer internally.'
isInitialDatabaseImportDone:
type: bool
description: 'If TRUE, the database import is finished. This value is handled within the install tool step installer internally.'
systemMaintainers:
type: array
description: 'A list of backend user IDs allowed to access the Install Tool'
......
......@@ -45,8 +45,6 @@ return [
'jpg_quality' => '80',
],
'SYS' => [
'isInitialInstallationInProgress' => true,
'isInitialDatabaseImportDone' => false,
'sitename' => 'New TYPO3 site',
],
];
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Install\Controller;
/*
......@@ -14,139 +15,52 @@ namespace TYPO3\CMS\Install\Controller;
* The TYPO3 project - inspiring people to share!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Controller\Action\Common\LoginForm;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
* Controller abstract for shared parts of Tool, Step and Ajax controller
* Controller abstract for shared parts of the install tool
*/
class AbstractController
{
/**
* @var array List of valid action names that need authentication
*/
protected $authenticationActions = [];
/**
* Show login form
* Helper method to initialize a standalone view instance.
*
* @param ServerRequestInterface $request
* @param FlashMessage $message Optional status message
* @return ResponseInterface
*/
protected function loginForm(ServerRequestInterface $request, FlashMessage $message = null): ResponseInterface
{
/** @var LoginForm $action */
$action = GeneralUtility::makeInstance(LoginForm::class);
$action->setController('common');
$action->setAction('login');
$action->setContext($request->getAttribute('context'));
$action->setToken($this->generateTokenForAction('login'));
$action->setPostValues($request->getParsedBody()['install'] ?? []);
if ($message) {
$action->setMessages([$message]);
}
return $action->handle();
}
/**
* Generate token for specific action
*
* @param string $action Action name
* @return string Form protection token
* @throws Exception
* @param string $templatePath
* @return StandaloneView
* @internal param string $template
*/
protected function generateTokenForAction($action = null)
protected function initializeStandaloneView(ServerRequestInterface $request, string $templatePath): StandaloneView
{
if ($action === '') {
throw new Exception(
'Token must have a valid action name',
1369326592
);
}
/** @var $formProtection \TYPO3\CMS\Core\FormProtection\InstallToolFormProtection */
$formProtection = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(
\TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class
);
return $formProtection->generateToken('installTool', $action);
$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'] ?? '',
]);
return $view;
}
/**
* Check given action name is one of the allowed actions.
* Some actions like the database analyzer and the upgrade wizards need additional
* bootstrap actions performed.
*
* @param string $action Given action to validate
* @throws Exception
* 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 validateAuthenticationAction($action)
protected function loadExtLocalconfDatabaseAndExtTables()
{
if (!in_array($action, $this->authenticationActions)) {
throw new Exception(
$action . ' is not a valid authentication action',
1369345838
);
}
}
/**
* Retrieve parameter from GET or POST and sanitize
*
* @throws Exception
* @param string $action requested action
* @return string Empty string if no action is given or sanitized action string
*/
protected function sanitizeAction($action = '')
{
if ($action !== ''
&& $action !== 'login'
&& $action !== 'loginForm'
&& $action !== 'logout'
&& !in_array($action, $this->authenticationActions)
) {
throw new Exception(
'Invalid action ' . $action,
1369325619
);
}
return $action;
}
/**
* HTTP redirect to self, preserving allowed GET variables.
*
* @param ServerRequestInterface $request
* @param string $action Set specific action for next request, used in step controller to specify next step
* @throws Exception\RedirectLoopException
* @return ResponseInterface
*/
public function redirectToSelfAction(ServerRequestInterface $request, string $action = ''): ResponseInterface
{
$redirectCount = $request->getQueryParams()['install']['redirectCount'] ?? $request->getParsedBody()['install']['redirectCount'] ?? -1;
// Current redirect count
$redirectCount = (int)($redirectCount)+1;
if ($redirectCount >= 15) {
// Abort a redirect loop by throwing an exception. Calling this method
// some times in a row is ok, but break a loop if this happens too often.
throw new Exception\RedirectLoopException(
'Redirect loop aborted. If this message is shown again after a reload,' .
' your setup is so weird that the install tool is unable to handle it.' .
' Please make sure to remove the "install[redirectCount]" parameter from your request or' .
' restart the install tool from the backend navigation.',
1380581244
);
}
$parameters = [
'install[redirectCount]=' . $redirectCount
];
// Add action if specified
if ($action !== '') {
$parameters[] = 'install[action]=' . $action;
}
$redirectLocation = GeneralUtility::getIndpEnv('TYPO3_REQUEST_SCRIPT') . '?' . implode('&', $parameters);
return new RedirectResponse($redirectLocation, 303);
\TYPO3\CMS\Core\Core\Bootstrap::getInstance()
->ensureClassLoadingInformationExists()
->loadTypo3LoadedExtAndExtLocalconf(false)
->unsetReservedGlobalVariables()
->loadBaseTca(false)
->loadExtTables(false);
}
}
<?php
namespace TYPO3\CMS\Install\Controller\Action;
/*
* 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 TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
* General purpose controller action helper methods and bootstrap
*/
abstract class AbstractAction implements ActionInterface
{
/**
* @var StandaloneView
*/
protected $view = null;
/**
* @var string Name of controller. One of the strings 'step', 'tool' or 'common'
*/
protected $controller = '';
/**
* @var string Name of target action, set by controller
*/
protected $action = '';
/**
* @var string Form token for CSRF protection
*/
protected $token = '';
/**
* @var array Values in $_POST['install']
*/
protected $postValues = [];
/**
* @var array<\TYPO3\CMS\Install\Status\StatusInterface> Optional status message from controller
*/
protected $messages = [];
/**
* @var string
*/
protected $context = self::CONTEXT_STANDALONE;
/**
* Handles the action
*
* @return ResponseInterface
*/
public function handle(): ResponseInterface
{
$this->initializeHandle();
return new HtmlResponse($this->executeAction(), 200, [
'Cache-Control' => 'no-cache, must-revalidate',
'Pragma' => 'no-cache'
]);
}
/**
* Initialize the handle action, sets up fluid stuff and assigns default variables.
*/
protected function initializeHandle()
{
$viewRootPath = GeneralUtility::getFileAbsFileName('EXT:install/Resources/Private/');
$controllerActionDirectoryName = ucfirst($this->controller);
$mainTemplate = ucfirst($this->action);
$this->view = GeneralUtility::makeInstance(StandaloneView::class);
$this->view->getRequest()->setControllerExtensionName('Install');
$this->view->setTemplatePathAndFilename($viewRootPath . 'Templates/Action/' . $controllerActionDirectoryName . '/' . $mainTemplate . '.html');
$this->view->setLayoutRootPaths([$viewRootPath . 'Layouts/']);
$this->view->setPartialRootPaths([$viewRootPath . 'Partials/']);
$this->view
// time is used in js and css as parameter to force loading of resources
->assign('time', time())
->assign('action', $this->action)
->assign('controller', $this->controller)
->assign('token', $this->token)
->assign('context', $this->context)
->assign('backendContext', $this->context === self::CONTEXT_BACKEND)
->assign('messages', $this->messages)
->assign('typo3Version', TYPO3_version)
->assign('siteName', $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']);
}
/**
* Executes the action
*
* @return string|array Rendered content
*/
abstract protected function executeAction();
/**
* Set form protection token
*
* @param string $token Form protection token
*/
public function setToken($token)
{
$this->token = $token;
}
/**
* Set action group. Either string 'step', 'tool' or 'common'
*
* @param string $controller Controller name
*/
public function setController($controller)
{
$this->controller = $controller;
}
/**
* Set action name. This is usually similar to the class name,
* only for loginForm, the action is login
*
* @param string $action Name of target action for forms
*/
public function setAction($action)
{
$this->action = $action;
}
/**
* Set POST form values of install tool
*
* @param array $postValues
*/
public function setPostValues(array $postValues)
{
$this->postValues = $postValues;
}
/**
* Status messages from controller
*
* @param array<\TYPO3\CMS\Install\Status\StatusInterface> $messages
*/
public function setMessages(array $messages = [])
{
$this->messages = $messages;
}
/**
* Context determines if the install tool is called within backend or standalone
*
* @param $context string One of the `CONTEXT_*` constants.
*/
public function setContext($context)
{
switch ($context) {
case self::CONTEXT_STANDALONE:
case self::CONTEXT_BACKEND:
$this->context = $context;
break;
default:
$this->context = self::CONTEXT_STANDALONE;
}
}
/**
* 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::getInstance()
->ensureClassLoadingInformationExists()
->loadTypo3LoadedExtAndExtLocalconf(false)
->unsetReservedGlobalVariables()
->loadBaseTca(false)
->loadExtTables(false);
}
/**
* This function returns a salted hashed key.
*
* @param string $password
* @return string
*/
protected function getHashedPassword($password)
{
$saltFactory = \TYPO3\CMS\Saltedpasswords\Salt\SaltFactory::getSaltingInstance(null, 'BE');
return $saltFactory->getHashedPassword($password);
}
}
<?php
namespace TYPO3\CMS\Install\Controller\Action;
/*
* 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;
/**
* General action interface
*/
interface ActionInterface
{
const CONTEXT_STANDALONE = 'standalone';
const CONTEXT_BACKEND = 'backend';
/**
* Handle this action
*
* @return ResponseInterface Rendered content
*/
public function handle(): ResponseInterface;
/**
* Set form protection token
*
* @param string $token Form protection token
*/
public function setToken($token);
/**
* Set controller, Either string 'step', 'tool' or 'common'
*
* @param string $controller Controller name
*/
public function setController($controller);
/**
* Set action name. This is usually similar to the class name,
* only for loginForm, the action is login
*
* @param string $action Name of target action for forms
*/
public function setAction($action);
/**
* Set the context name, must be one of the `CONTEXT_*` constants.
*
* @param string $context
*/
public function setContext($context);
/**
* Set POST values
*
* @param array $postValues List of values submitted via POST
*/
public function setPostValues(array $postValues);
/**
* Status messages from controller
*
* @param array<\TYPO3\CMS\Install\Status\StatusInterface> $messages
*/
public function setMessages(array $messages = []);
}
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Install\Controller\Action\Ajax;
/*
* 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 TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Controller\Action\AbstractAction;
use TYPO3\CMS\Install\View\JsonView;
/**
* General purpose AJAX controller action helper methods and bootstrap
*/
abstract class AbstractAjaxAction extends AbstractAction
{
/**
* @var JsonView
*/
protected $view;
/**
* @param JsonView $view
*/
public function __construct(JsonView $view = null)
{
$this->view = $view ?: GeneralUtility::makeInstance(JsonView::class);
}
/**
* AbstractAjaxAction still overwrites $this->view with StandaloneView, which is
* shut off here.
*/
protected function initializeHandle()
{
// Deliberately empty
}
/**
* Handles the action.
*
* @return ResponseInterface Rendered content
*/
public function handle(): ResponseInterface
{
$this->initializeHandle();
return new JsonResponse((array)$this->executeAction(), 200, [
'Cache-Control' => 'no-cache, must-revalidate',
'Pragma' => 'no-cache'
]);
}
}
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Install\Controller\Action\Ajax;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under