vendor/contao/installation-bundle/src/Controller/InstallationController.php line 50

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of Contao.
  5.  *
  6.  * (c) Leo Feyer
  7.  *
  8.  * @license LGPL-3.0-or-later
  9.  */
  10. namespace Contao\InstallationBundle\Controller;
  11. use Contao\Environment;
  12. use Contao\InstallationBundle\Config\ParameterDumper;
  13. use Contao\InstallationBundle\Database\ConnectionFactory;
  14. use Contao\InstallationBundle\Event\ContaoInstallationEvents;
  15. use Contao\InstallationBundle\Event\InitializeApplicationEvent;
  16. use Contao\Validator;
  17. use Doctrine\DBAL\Exception;
  18. use Symfony\Component\DependencyInjection\ContainerAwareInterface;
  19. use Symfony\Component\DependencyInjection\ContainerAwareTrait;
  20. use Symfony\Component\Filesystem\Filesystem;
  21. use Symfony\Component\Filesystem\Path;
  22. use Symfony\Component\Finder\Finder;
  23. use Symfony\Component\Finder\SplFileInfo;
  24. use Symfony\Component\HttpFoundation\RedirectResponse;
  25. use Symfony\Component\HttpFoundation\Response;
  26. use Symfony\Component\Routing\Annotation\Route;
  27. /**
  28.  * @Route("%contao.backend.route_prefix%", defaults={"_scope" = "backend", "_token_check" = true})
  29.  *
  30.  * @internal
  31.  */
  32. class InstallationController implements ContainerAwareInterface
  33. {
  34.     use ContainerAwareTrait;
  35.     private array $context = [
  36.         'has_admin' => false,
  37.         'hide_admin' => false,
  38.         'sql_message' => '',
  39.     ];
  40.     /**
  41.      * @Route("/install", name="contao_install")
  42.      */
  43.     public function installAction(): Response
  44.     {
  45.         if (null !== ($response $this->initializeApplication())) {
  46.             return $response;
  47.         }
  48.         if ($this->container->has('contao.framework')) {
  49.             $this->container->get('contao.framework')->initialize();
  50.         }
  51.         $installTool $this->container->get('contao_installation.install_tool');
  52.         if ($installTool->isLocked()) {
  53.             return $this->render('locked.html.twig');
  54.         }
  55.         if (!$installTool->canWriteFiles()) {
  56.             return $this->render('not_writable.html.twig');
  57.         }
  58.         if ($installTool->shouldAcceptLicense()) {
  59.             return $this->acceptLicense();
  60.         }
  61.         if ('' === $installTool->getConfig('installPassword')) {
  62.             return $this->setPassword();
  63.         }
  64.         if (!$this->container->get('contao_installation.install_tool_user')->isAuthenticated()) {
  65.             return $this->login();
  66.         }
  67.         if (!$installTool->canConnectToDatabase($this->getContainerParameter('database_name'))) {
  68.             return $this->setUpDatabaseConnection();
  69.         }
  70.         $this->warmUpSymfonyCache();
  71.         if ($installTool->hasOldDatabase()) {
  72.             return $this->render('old_database.html.twig');
  73.         }
  74.         if ($installTool->hasConfigurationError($this->context)) {
  75.             return $this->render('configuration_error.html.twig');
  76.         }
  77.         $this->runDatabaseUpdates();
  78.         $installTool->checkStrictMode($this->context);
  79.         if (null !== ($response $this->adjustDatabaseTables())) {
  80.             return $response;
  81.         }
  82.         if (null !== ($response $this->importExampleWebsite())) {
  83.             return $response;
  84.         }
  85.         if (null !== ($response $this->createAdminUser())) {
  86.             return $response;
  87.         }
  88.         return $this->render('main.html.twig'$this->context);
  89.     }
  90.     private function initializeApplication(): ?Response
  91.     {
  92.         $event = new InitializeApplicationEvent();
  93.         $this->container->get('event_dispatcher')->dispatch($eventContaoInstallationEvents::INITIALIZE_APPLICATION);
  94.         if ($event->hasOutput()) {
  95.             return $this->render('initialize.html.twig', [
  96.                 'output' => $event->getOutput(),
  97.             ]);
  98.         }
  99.         return null;
  100.     }
  101.     /**
  102.      * Renders a form to accept the license.
  103.      *
  104.      * @return Response|RedirectResponse
  105.      */
  106.     private function acceptLicense(): Response
  107.     {
  108.         $request $this->container->get('request_stack')->getCurrentRequest();
  109.         if (null === $request) {
  110.             throw new \RuntimeException('The request stack did not contain a request');
  111.         }
  112.         if ('tl_license' !== $request->request->get('FORM_SUBMIT')) {
  113.             return $this->render('license.html.twig');
  114.         }
  115.         $this->container->get('contao_installation.install_tool')->persistConfig('licenseAccepted'true);
  116.         return $this->getRedirectResponse();
  117.     }
  118.     /**
  119.      * Renders a form to set the install tool password.
  120.      *
  121.      * @return Response|RedirectResponse
  122.      */
  123.     private function setPassword(): Response
  124.     {
  125.         $request $this->container->get('request_stack')->getCurrentRequest();
  126.         if (null === $request) {
  127.             throw new \RuntimeException('The request stack did not contain a request');
  128.         }
  129.         if ('tl_password' !== $request->request->get('FORM_SUBMIT')) {
  130.             return $this->render('password.html.twig');
  131.         }
  132.         $password $request->request->get('password');
  133.         $installTool $this->container->get('contao_installation.install_tool');
  134.         $minlength $installTool->getConfig('minPasswordLength');
  135.         // The password is too short
  136.         if (mb_strlen($password) < $minlength) {
  137.             return $this->render('password.html.twig', [
  138.                 'error' => sprintf($this->trans('password_too_short'), $minlength),
  139.             ]);
  140.         }
  141.         $installTool->persistConfig('installPassword'password_hash($passwordPASSWORD_DEFAULT));
  142.         $this->container->get('contao_installation.install_tool_user')->setAuthenticated(true);
  143.         return $this->getRedirectResponse();
  144.     }
  145.     /**
  146.      * Renders a form to log in.
  147.      *
  148.      * @return Response|RedirectResponse
  149.      */
  150.     private function login(): Response
  151.     {
  152.         $request $this->container->get('request_stack')->getCurrentRequest();
  153.         if (null === $request) {
  154.             throw new \RuntimeException('The request stack did not contain a request');
  155.         }
  156.         if ('tl_login' !== $request->request->get('FORM_SUBMIT')) {
  157.             return $this->render('login.html.twig');
  158.         }
  159.         $installTool $this->container->get('contao_installation.install_tool');
  160.         $verified password_verify(
  161.             $request->request->get('password'),
  162.             $installTool->getConfig('installPassword')
  163.         );
  164.         if (!$verified) {
  165.             $installTool->increaseLoginCount();
  166.             return $this->render('login.html.twig', [
  167.                 'error' => $this->trans('invalid_password'),
  168.             ]);
  169.         }
  170.         $installTool->resetLoginCount();
  171.         $this->container->get('contao_installation.install_tool_user')->setAuthenticated(true);
  172.         return $this->getRedirectResponse();
  173.     }
  174.     /**
  175.      * The method preserves the container directory inside the cache folder,
  176.      * because Symfony will throw a "compile error" exception if it is deleted
  177.      * in the middle of a request.
  178.      */
  179.     private function purgeSymfonyCache(): void
  180.     {
  181.         $filesystem = new Filesystem();
  182.         $cacheDir $this->getContainerParameter('kernel.cache_dir');
  183.         $ref = new \ReflectionObject($this->container);
  184.         $containerDir basename(Path::getDirectory($ref->getFileName()));
  185.         /** @var array<SplFileInfo> $finder */
  186.         $finder Finder::create()
  187.             ->depth(0)
  188.             ->exclude($containerDir)
  189.             ->in($cacheDir)
  190.         ;
  191.         foreach ($finder as $file) {
  192.             $filesystem->remove($file->getPathname());
  193.         }
  194.         if (\function_exists('opcache_reset')) {
  195.             opcache_reset();
  196.         }
  197.         if (\function_exists('apc_clear_cache') && !\ini_get('apc.stat')) {
  198.             apc_clear_cache();
  199.         }
  200.     }
  201.     /**
  202.      * The method runs the optional cache warmers, because the cache will only
  203.      * have the non-optional stuff at this time.
  204.      */
  205.     private function warmUpSymfonyCache(): void
  206.     {
  207.         $cacheDir $this->getContainerParameter('kernel.cache_dir');
  208.         if (file_exists(Path::join($cacheDir'contao/config/config.php'))) {
  209.             return;
  210.         }
  211.         $warmer $this->container->get('cache_warmer');
  212.         if (!$this->getContainerParameter('kernel.debug')) {
  213.             $warmer->enableOptionalWarmers();
  214.         }
  215.         $warmer->warmUp($cacheDir);
  216.         if (\function_exists('opcache_reset')) {
  217.             opcache_reset();
  218.         }
  219.         if (\function_exists('apc_clear_cache') && !\ini_get('apc.stat')) {
  220.             apc_clear_cache();
  221.         }
  222.     }
  223.     /**
  224.      * Renders a form to set up the database connection.
  225.      *
  226.      * @return Response|RedirectResponse
  227.      */
  228.     private function setUpDatabaseConnection(): Response
  229.     {
  230.         $request $this->container->get('request_stack')->getCurrentRequest();
  231.         if (null === $request) {
  232.             throw new \RuntimeException('The request stack did not contain a request');
  233.         }
  234.         // Only warn the user if the connection fails and the env component is used
  235.         if (false !== getenv('DATABASE_URL')) {
  236.             return $this->render('misconfigured_database_url.html.twig');
  237.         }
  238.         if ('tl_database_login' !== $request->request->get('FORM_SUBMIT')) {
  239.             return $this->render('database.html.twig');
  240.         }
  241.         $parameters = [
  242.             'parameters' => [
  243.                 'database_host' => $request->request->get('dbHost'),
  244.                 'database_port' => $request->request->get('dbPort'),
  245.                 'database_user' => $request->request->get('dbUser'),
  246.                 'database_password' => $request->request->get('dbPassword') ?: null,
  247.                 'database_name' => $request->request->get('dbName'),
  248.             ],
  249.         ];
  250.         if (false !== strpos($parameters['parameters']['database_name'], '.')) {
  251.             return $this->render('database.html.twig'array_merge(
  252.                 $parameters,
  253.                 ['database_error' => $this->trans('database_dot_in_dbname')]
  254.             ));
  255.         }
  256.         $connection ConnectionFactory::create($parameters);
  257.         $installTool $this->container->get('contao_installation.install_tool');
  258.         $installTool->setConnection($connection);
  259.         if (!$installTool->canConnectToDatabase($parameters['parameters']['database_name'])) {
  260.             return $this->render('database.html.twig'array_merge(
  261.                 $parameters,
  262.                 ['database_error' => $this->trans('database_could_not_connect')]
  263.             ));
  264.         }
  265.         $databaseVersion null;
  266.         try {
  267.             $databaseVersion $connection->getWrappedConnection()->getServerVersion();
  268.         } catch (\Throwable $exception) {
  269.             // Ignore server version detection errors
  270.         }
  271.         if ($databaseVersion) {
  272.             $parameters['parameters']['database_version'] = $databaseVersion;
  273.         }
  274.         $dumper = new ParameterDumper($this->getContainerParameter('kernel.project_dir'));
  275.         $dumper->setParameters($parameters);
  276.         $dumper->dump();
  277.         $this->purgeSymfonyCache();
  278.         return $this->getRedirectResponse();
  279.     }
  280.     private function runDatabaseUpdates(): void
  281.     {
  282.         $this->context['sql_message'] = implode(
  283.             '<br>',
  284.             array_map('htmlspecialchars'$this->container->get('contao_installation.install_tool')->runMigrations())
  285.         );
  286.     }
  287.     /**
  288.      * Renders a form to adjust the database tables.
  289.      */
  290.     private function adjustDatabaseTables(): ?RedirectResponse
  291.     {
  292.         $this->container->get('contao_installation.install_tool')->handleRunOnce();
  293.         $installer $this->container->get('contao_installation.database.installer');
  294.         $this->context['sql_form'] = $installer->getCommands();
  295.         $request $this->container->get('request_stack')->getCurrentRequest();
  296.         if (null === $request) {
  297.             throw new \RuntimeException('The request stack did not contain a request');
  298.         }
  299.         if ('tl_database_update' !== $request->request->get('FORM_SUBMIT')) {
  300.             return null;
  301.         }
  302.         $sql $request->request->get('sql');
  303.         if (!empty($sql) && \is_array($sql)) {
  304.             foreach ($sql as $hash) {
  305.                 $installer->execCommand($hash);
  306.             }
  307.         }
  308.         return $this->getRedirectResponse();
  309.     }
  310.     /**
  311.      * Renders a form to import the example website.
  312.      */
  313.     private function importExampleWebsite(): ?RedirectResponse
  314.     {
  315.         $installTool $this->container->get('contao_installation.install_tool');
  316.         $templates $installTool->getTemplates();
  317.         $this->context['templates'] = $templates;
  318.         if ($installTool->getConfig('exampleWebsite')) {
  319.             $this->context['import_date'] = date('Y-m-d H:i'$installTool->getConfig('exampleWebsite'));
  320.         }
  321.         $request $this->container->get('request_stack')->getCurrentRequest();
  322.         if (null === $request) {
  323.             throw new \RuntimeException('The request stack did not contain a request');
  324.         }
  325.         if ('tl_template_import' !== $request->request->get('FORM_SUBMIT')) {
  326.             return null;
  327.         }
  328.         $template $request->request->get('template');
  329.         if ('' === $template || !\in_array($template$templatestrue)) {
  330.             $this->context['import_error'] = $this->trans('import_empty_source');
  331.             return null;
  332.         }
  333.         try {
  334.             $installTool->importTemplate($template'1' === $request->request->get('preserve'));
  335.         } catch (Exception $e) {
  336.             $installTool->persistConfig('exampleWebsite'null);
  337.             $installTool->logException($e);
  338.             for ($rootException $enull !== $rootException->getPrevious(); $rootException $rootException->getPrevious()) {
  339.             }
  340.             $this->context['import_error'] = $this->trans('import_exception')."\n".$rootException->getMessage();
  341.             return null;
  342.         }
  343.         $installTool->persistConfig('exampleWebsite'time());
  344.         return $this->getRedirectResponse();
  345.     }
  346.     private function createAdminUser(): ?RedirectResponse
  347.     {
  348.         $installTool $this->container->get('contao_installation.install_tool');
  349.         if (!$installTool->hasTable('tl_user')) {
  350.             $this->context['hide_admin'] = true;
  351.             return null;
  352.         }
  353.         if ($installTool->hasAdminUser()) {
  354.             $this->context['has_admin'] = true;
  355.             return null;
  356.         }
  357.         $request $this->container->get('request_stack')->getCurrentRequest();
  358.         if (null === $request) {
  359.             throw new \RuntimeException('The request stack did not contain a request');
  360.         }
  361.         if ('tl_admin' !== $request->request->get('FORM_SUBMIT')) {
  362.             return null;
  363.         }
  364.         $username $request->request->get('username');
  365.         $name $request->request->get('name');
  366.         $email $request->request->get('email');
  367.         $password $request->request->get('password');
  368.         $this->context['admin_username_value'] = $username;
  369.         $this->context['admin_name_value'] = $name;
  370.         $this->context['admin_email_value'] = $email;
  371.         $this->context['admin_password_value'] = $password;
  372.         // All fields are mandatory
  373.         if ('' === $username || '' === $name || '' === $email || '' === $password) {
  374.             $this->context['admin_error'] = $this->trans('admin_error');
  375.             return null;
  376.         }
  377.         // Do not allow special characters in usernames
  378.         if (!Validator::isExtendedAlphanumeric($username)) {
  379.             $this->context['admin_username_error'] = $this->trans('admin_error_extnd');
  380.             return null;
  381.         }
  382.         // The username must not contain whitespace characters (see #4006)
  383.         if (false !== strpos($username' ')) {
  384.             $this->context['admin_username_error'] = $this->trans('admin_error_no_space');
  385.             return null;
  386.         }
  387.         // Validate the e-mail address (see #6003)
  388.         if (!Validator::isEmail($email)) {
  389.             $this->context['admin_email_error'] = $this->trans('admin_error_email');
  390.             return null;
  391.         }
  392.         $minlength $installTool->getConfig('minPasswordLength');
  393.         // The password is too short
  394.         if (mb_strlen($password) < $minlength) {
  395.             $this->context['admin_password_error'] = sprintf($this->trans('password_too_short'), $minlength);
  396.             return null;
  397.         }
  398.         // Password and username are the same
  399.         if ($password === $username) {
  400.             $this->context['admin_password_error'] = sprintf($this->trans('admin_error_password_user'), $minlength);
  401.             return null;
  402.         }
  403.         $installTool->persistConfig('adminEmail'$email);
  404.         $installTool->persistAdminUser($username$name$email$password$request->getLocale());
  405.         return $this->getRedirectResponse();
  406.     }
  407.     private function render(string $name, array $context = []): Response
  408.     {
  409.         return new Response(
  410.             $this->container->get('twig')->render(
  411.                 '@ContaoInstallation/'.$name,
  412.                 $this->addDefaultsToContext($context)
  413.             )
  414.         );
  415.     }
  416.     private function trans(string $key): string
  417.     {
  418.         return $this->container->get('translator')->trans($key, [], 'ContaoInstallationBundle');
  419.     }
  420.     /**
  421.      * Returns a redirect response to reload the page.
  422.      */
  423.     private function getRedirectResponse(): RedirectResponse
  424.     {
  425.         $request $this->container->get('request_stack')->getCurrentRequest();
  426.         if (null === $request) {
  427.             throw new \RuntimeException('The request stack did not contain a request');
  428.         }
  429.         return new RedirectResponse($request->getRequestUri());
  430.     }
  431.     /**
  432.      * Adds the default values to the context.
  433.      *
  434.      * @return array<string,string>
  435.      */
  436.     private function addDefaultsToContext(array $context): array
  437.     {
  438.         $context array_merge($this->context$context);
  439.         if (!isset($context['request_token'])) {
  440.             $context['request_token'] = $this->getRequestToken();
  441.         }
  442.         if (!isset($context['language'])) {
  443.             $context['language'] = $this->container->get('translator')->getLocale();
  444.         }
  445.         // Backwards compatibility
  446.         if (!isset($context['ua'])) {
  447.             $context['ua'] = $this->getUserAgentString();
  448.         }
  449.         if (!isset($context['path'])) {
  450.             $request $this->container->get('request_stack')->getCurrentRequest();
  451.             if (null === $request) {
  452.                 throw new \RuntimeException('The request stack did not contain a request');
  453.             }
  454.             $context['host'] = $request->getHost();
  455.             $context['path'] = $request->getBasePath();
  456.         }
  457.         return $context;
  458.     }
  459.     private function getRequestToken(): string
  460.     {
  461.         $tokenName $this->getContainerParameter('contao.csrf_token_name');
  462.         if (null === $tokenName) {
  463.             return '';
  464.         }
  465.         return $this->container->get('contao.csrf.token_manager')->getToken($tokenName)->getValue();
  466.     }
  467.     /**
  468.      * @return string|bool|null
  469.      */
  470.     private function getContainerParameter(string $name)
  471.     {
  472.         if ($this->container->hasParameter($name)) {
  473.             return $this->container->getParameter($name);
  474.         }
  475.         return null;
  476.     }
  477.     private function getUserAgentString(): string
  478.     {
  479.         if (!$this->container->has('contao.framework') || !$this->container->get('contao.framework')->isInitialized()) {
  480.             return '';
  481.         }
  482.         return Environment::get('agent')->class;
  483.     }
  484. }