From d545e7e09d1fc5cec70e56dbaadac26b27cd3a76 Mon Sep 17 00:00:00 2001 From: Michael Kaufmann Date: Wed, 1 Jun 2022 14:05:18 +0200 Subject: [PATCH] add CLI install command (more testing tbd) Signed-off-by: Michael Kaufmann --- bin/froxlor-cli | 2 + lib/Froxlor/Cli/InstallCommand.php | 195 +++++++++++++++++++ lib/Froxlor/Cli/install.functions.php | 36 ++++ lib/Froxlor/Install/Install.php | 51 ++--- lib/formfields/install/formfield.install.php | 2 +- 5 files changed, 260 insertions(+), 26 deletions(-) create mode 100644 lib/Froxlor/Cli/InstallCommand.php create mode 100644 lib/Froxlor/Cli/install.functions.php diff --git a/bin/froxlor-cli b/bin/froxlor-cli index 80b62ebc..b6a62b4b 100755 --- a/bin/froxlor-cli +++ b/bin/froxlor-cli @@ -32,6 +32,7 @@ use Froxlor\Cli\ConfigServices; use Froxlor\Cli\PhpSessionclean; use Froxlor\Cli\SwitchServerIp; use Froxlor\Cli\UpdateCommand; +use Froxlor\Cli\InstallCommand; use Froxlor\Froxlor; // validate correct php version @@ -53,4 +54,5 @@ $application->add(new ConfigServices()); $application->add(new PhpSessionclean()); $application->add(new SwitchServerIp()); $application->add(new UpdateCommand()); +$application->add(new InstallCommand()); $application->run(); diff --git a/lib/Froxlor/Cli/InstallCommand.php b/lib/Froxlor/Cli/InstallCommand.php new file mode 100644 index 00000000..ab394148 --- /dev/null +++ b/lib/Froxlor/Cli/InstallCommand.php @@ -0,0 +1,195 @@ + + * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 + */ + +namespace Froxlor\Cli; + +use Exception; +use Froxlor\Froxlor; +use Froxlor\Install\Install; +use Froxlor\Install\Install\Core; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +final class InstallCommand extends Command +{ + + private $io = null; + + private $formfielddata = []; + + protected function configure() + { + $this->setName('froxlor:install'); + $this->setDescription('Installation process to use instead of web-ui'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $result = self::SUCCESS; + + if (file_exists(Froxlor::getInstallDir() . '/lib/userdata.inc.php')) { + $output->writeln("froxlor seems to be installed already."); + return self::INVALID; + } + + require __DIR__ . '/install.functions.php'; + + $this->io = new SymfonyStyle($input, $output); + + $this->io->title('Froxlor installation'); + + $extended = $this->io->confirm('Use advanced installation mode?', false); + + // set a few defaults CLI cannot know + $_SERVER['SERVER_SOFTWARE'] = 'apache'; + $host = []; + exec('hostname -f', $host); + $_SERVER['SERVER_NAME'] = $host[0] ?? ''; + $ips = []; + exec('hostname -I', $ips); + $ips = explode(" ", $ips[0]); + // ipv4 address? + $_SERVER['SERVER_ADDR'] = filter_var($ips[0] ?? "", FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? ($ips[0] ?? '') : ''; + if (empty($_SERVER['SERVER_ADDR'])) { + // possible ipv6 address? + $_SERVER['SERVER_ADDR'] = filter_var($ips[0] ?? "", FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? ($ips[0] ?? '') : ''; + } + + $result = $this->showStep(0, $extended); + + return $result; + } + + private function showStep(int $step = 0, bool $extended = false): int + { + $result = self::SUCCESS; + $inst = new Install(['step' => $step, 'extended' => $extended]); + switch ($step) { + case 0: + $this->io->section(lng('install.preflight')); + $crresult = $inst->checkRequirements(); + $this->io->info($crresult['text']); + if (!empty($crresult['criticals'])) { + foreach ($crresult['criticals'] as $ctype => $critical) { + if (!empty($ctype) && $ctype == 'wrong_ownership') { + $this->io->error(lng('install.errors.' . $ctype, [$critical['user'], $critical['group']])); + } elseif (!empty($ctype) && $ctype == 'missing_extensions') { + $this->io->error([ + lng('install.errors.' . $ctype), + implode("\n", $critical) + ]); + } else { + $this->io->error($critical); + } + } + $result = self::FAILURE; + } + if (!empty($crresult['suggestions'])) { + foreach ($crresult['suggestions'] as $ctype => $suggestion) { + if ($ctype == 'missing_extensions') { + $this->io->warning([ + lng('install.errors.suggestedextensions'), + implode("\n", $suggestion) + ]); + } else { + $this->io->warning($suggestion); + } + } + } + if ($result == self::SUCCESS && $this->io->confirm('Continue?', true)) { + return $this->showStep(++$step, $extended); + } + break; + case 1: + case 2: + case 3: + $section = $inst->formfield['install']['sections']['step' . $step] ?? []; + $this->io->section($section['title']); + $this->io->note($section['description']); + foreach ($section['fields'] as $fieldname => $fielddata) { + if ($extended == false && isset($fielddata['advanced']) && $fielddata['advanced'] == true) { + continue; + } + $fielddata['value'] = $this->formfielddata[$fieldname] ?? ($fielddata['value'] ?? null); + $fielddata['label'] = strip_tags(str_replace("
", " ", $fielddata['label'])); + if ($fielddata['type'] == 'password') { + $this->formfielddata[$fieldname] = $this->io->askHidden($fielddata['label'], function ($value) use ($fielddata) { + if (isset($fielddata['mandatory']) && $fielddata['mandatory'] && empty($value)) { + throw new \RuntimeException('You must enter a value.'); + } + return $value; + }); + } elseif ($fielddata['type'] == 'checkbox') { + $this->formfielddata[$fieldname] = $this->io->confirm($fielddata['label'], $fielddata['value'] ?? false); + } elseif ($fielddata['type'] == 'select') { + $this->formfielddata[$fieldname] = $this->io->choice($fielddata['label'], $fielddata['select_var'], $fielddata['selected'] ?? ''); + } else { + $this->formfielddata[$fieldname] = $this->io->ask($fielddata['label'], $fielddata['value'] ?? '', function ($value) use ($fielddata) { + if (isset($fielddata['mandatory']) && $fielddata['mandatory'] && empty($value)) { + throw new \RuntimeException('You must enter a value.'); + } + return $value; + }); + } + } + + try { + if ($step == 1) { +// $inst->checkDatabase($this->formfielddata); + } elseif ($step == 2) { +// $inst->checkAdminUser($this->formfielddata); + } elseif ($step == 3) { +// $inst->checkSystem($this->formfielddata); + } + } + catch (Exception $e) { + $this->io->error($e->getMessage()); + return $this->showStep($step, $extended); + } + return $this->showStep(++$step, $extended); + break; + case 4: + // do actual install with data from $this->formfielddata +// $core = new Core($this->formfielddata); +// $core->doInstall(); + $section = $inst->formfield['install']['sections']['step' . $step] ?? []; + $this->io->section($section['title']); + $this->io->note($section['description']); + $cmdfield = $section['fields']['system']; + $this->io->success([ + $cmdfield['label'], + $cmdfield['value'] + ]); + if ($this->io->confirm('Execute command now?', false)) { + exec($cmdfield['value']); + } + break; + } + return $result; + } +} diff --git a/lib/Froxlor/Cli/install.functions.php b/lib/Froxlor/Cli/install.functions.php new file mode 100644 index 00000000..155f27bd --- /dev/null +++ b/lib/Froxlor/Cli/install.functions.php @@ -0,0 +1,36 @@ + + * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 + */ + +use Froxlor\Language; + +function lng(string $identifier, array $arguments = []) +{ + return Language::getTranslation($identifier, $arguments); +} + +function old(string $identifier, string $default = null, string $session = null) +{ + return $default; +} diff --git a/lib/Froxlor/Install/Install.php b/lib/Froxlor/Install/Install.php index bd38c988..1f52156e 100644 --- a/lib/Froxlor/Install/Install.php +++ b/lib/Froxlor/Install/Install.php @@ -54,7 +54,7 @@ class Install 'mod_php' => 'mod_php (not recommended)', ]; - public function __construct() + public function __construct(array $cliData = []) { // get all supported OS // show list of available distro's @@ -78,28 +78,30 @@ class Install $this->formfield = require dirname(__DIR__, 3) . '/lib/formfields/install/formfield.install.php'; // set actual step - $this->currentStep = Request::get('step', 0); - $this->extendedView = Request::get('extended', 0); + $this->currentStep = $cliData['step'] ?? Request::get('step', 0); + $this->extendedView = $cliData['extended'] ?? Request::get('extended', 0); $this->maxSteps = count($this->formfield['install']['sections']); // set actual php version and extensions $this->phpVersion = phpversion(); $this->loadedExtensions = get_loaded_extensions(); - // set global variables - UI::twig()->addGlobal('install_mode', true); - UI::twig()->addGlobal('basehref', '../'); + if (empty($cliData)) { + // set global variables + UI::twig()->addGlobal('install_mode', true); + UI::twig()->addGlobal('basehref', '../'); - // unset session if user goes back to step 0 - if (isset($_SESSION['installation']) && $this->currentStep == 0) { - unset($_SESSION['installation']); - } + // unset session if user goes back to step 0 + if (isset($_SESSION['installation']) && $this->currentStep == 0) { + unset($_SESSION['installation']); + } - // check for url manipulation or wrong step - if ((isset($_SESSION['installation']['stepCompleted']) && ($this->currentStep + 1) > ($_SESSION['installation']['stepCompleted'] ?? 0)) - || (!isset($_SESSION['installation']['stepCompleted']) && $this->currentStep > 0) - ) { - $this->currentStep = isset($_SESSION['installation']['stepCompleted']) ? $_SESSION['installation']['stepCompleted'] + 1 : 1; + // check for url manipulation or wrong step + if ((isset($_SESSION['installation']['stepCompleted']) && ($this->currentStep + 1) > ($_SESSION['installation']['stepCompleted'] ?? 0)) + || (!isset($_SESSION['installation']['stepCompleted']) && $this->currentStep > 0) + ) { + $this->currentStep = isset($_SESSION['installation']['stepCompleted']) ? $_SESSION['installation']['stepCompleted'] + 1 : 1; + } } } @@ -206,7 +208,7 @@ class Install /** * @return array */ - private function checkRequirements(): array + public function checkRequirements(): array { // check whether we can read the userdata file if (!@touch(dirname(__DIR__, 2) . '/.~writecheck')) { @@ -286,7 +288,7 @@ class Install /** * @throws Exception */ - private function checkSystem(array $validatedData): void + public function checkSystem(array $validatedData): void { $serveripv4 = $validatedData['serveripv4'] ?? ''; $serveripv6 = $validatedData['serveripv6'] ?? ''; @@ -312,7 +314,7 @@ class Install /** * @throws Exception */ - private function checkAdminUser(array $validatedData): void + public function checkAdminUser(array $validatedData): void { $name = $validatedData['admin_name'] ?? 'Administrator'; $loginname = $validatedData['admin_user'] ?? ''; @@ -336,7 +338,7 @@ class Install /** * @throws Exception */ - private function checkDatabase(array $validatedData): void + public function checkDatabase(array $validatedData): void { $dsn = sprintf('mysql:host=%s;charset=utf8', $validatedData['mysql_host']); $pdo = new \PDO($dsn, $validatedData['mysql_root_user'], $validatedData['mysql_root_pass']); @@ -384,7 +386,7 @@ class Install private function guessWebserver(): ?string { - if (strtoupper(@php_sapi_name()) == "APACHE2HANDLER" || stristr($_SERVER['SERVER_SOFTWARE'], "apache/2")) { + if (strtoupper(@php_sapi_name()) == "APACHE2HANDLER" || stristr($_SERVER['SERVER_SOFTWARE'], "apache")) { return 'apache24'; } elseif (substr(strtoupper(@php_sapi_name()), 0, 8) == "LIGHTTPD" || stristr($_SERVER['SERVER_SOFTWARE'], "lighttpd")) { return 'lighttpd'; @@ -397,14 +399,13 @@ class Install private function guessDistribution(): ?string { // set default os. - $os_dist = array( - 'VERSION_CODENAME' => 'bullseye' - ); + $default = 'bullseye'; + // read os-release if (@file_exists('/etc/os-release')) { $os_dist = parse_ini_file('/etc/os-release', false); - return strtolower($os_dist['VERSION_CODENAME'] ?? ($os_dist['ID'] ?? null)); + return strtolower($os_dist['VERSION_CODENAME'] ?? ($os_dist['ID'] ?? $default)); } - return null; + return $default; } } diff --git a/lib/formfields/install/formfield.install.php b/lib/formfields/install/formfield.install.php index e7cc2fd1..5b37444c 100644 --- a/lib/formfields/install/formfield.install.php +++ b/lib/formfields/install/formfield.install.php @@ -180,7 +180,7 @@ return [ 'type' => 'select', 'mandatory' => true, 'select_var' => ['apache24' => 'Apache 2.4', 'nginx' => 'Nginx', 'lighttpd' => 'LigHTTPd'], - 'value' => old('webserver', $guessedWebserver, 'installation'), + 'selected' => old('webserver', $guessedWebserver, 'installation'), ], 'webserver_backend' => [ 'label' => lng('install.system.phpbackend'),