From f31c03250843bea3f6e4df56a8e0cfc7ba670303 Mon Sep 17 00:00:00 2001 From: Michael Kaufmann Date: Wed, 2 Oct 2024 15:27:42 +0200 Subject: [PATCH] add possibility to ask for potential update question in CLI updater and also pass them as options to override them; check whether mysql-user exists prior to DROP USER for mysql < 5.7 (as it is missing IF EXISTS options) Signed-off-by: Michael Kaufmann --- .../updates/preconfig/preconfig_0.10.inc.php | 2 +- lib/Froxlor/Cli/UpdateCommand.php | 147 +++++++++++++++++- .../Database/Manager/DbManagerMySQL.php | 30 ++-- lib/Froxlor/Install/Preconfig.php | 22 +-- 4 files changed, 175 insertions(+), 26 deletions(-) diff --git a/install/updates/preconfig/preconfig_0.10.inc.php b/install/updates/preconfig/preconfig_0.10.inc.php index a1711fda..fd68fd44 100644 --- a/install/updates/preconfig/preconfig_0.10.inc.php +++ b/install/updates/preconfig/preconfig_0.10.inc.php @@ -34,7 +34,7 @@ $return = []; if (Update::versionInUpdate($current_db_version, '202004140')) { $has_preconfig = true; $description = 'Froxlor can now optionally validate the dns entries of domains that request Lets Encrypt certificates to reduce dns-related problems (e.g. freshly registered domain or updated a-record).'; - $question = 'Validate DNS of domains when using Lets Encrypt '; + $question = 'Validate DNS of domains when using Lets Encrypt'; $return['system_le_domain_dnscheck'] = [ 'type' => 'checkbox', 'value' => 1, diff --git a/lib/Froxlor/Cli/UpdateCommand.php b/lib/Froxlor/Cli/UpdateCommand.php index 65b063e6..566cd7a6 100644 --- a/lib/Froxlor/Cli/UpdateCommand.php +++ b/lib/Froxlor/Cli/UpdateCommand.php @@ -28,13 +28,17 @@ namespace Froxlor\Cli; use Exception; use Froxlor\Froxlor; use Froxlor\Install\AutoUpdate; +use Froxlor\Install\Preconfig; use Froxlor\Install\Update; use Froxlor\Settings; use Froxlor\System\Mailer; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Style\SymfonyStyle; final class UpdateCommand extends CliCommand { @@ -44,6 +48,8 @@ final class UpdateCommand extends CliCommand $this->setName('froxlor:update'); $this->setDescription('Check for newer version and update froxlor'); $this->addOption('check-only', 'c', InputOption::VALUE_NONE, 'Only check for newer version and exit') + ->addOption('show-update-options', 'o', InputOption::VALUE_NONE, 'Show possible update option parameter for the update if any. Only usable in combination with "check-only".') + ->addOption('update-options', 'O', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Parameter list of update options.') ->addOption('database', 'd', InputOption::VALUE_NONE, 'Only run database updates in case updates are done via apt or manually.') ->addOption('mail-notify', 'm', InputOption::VALUE_NONE, 'Additionally inform administrator via email if a newer version was found') ->addOption('yes-to-all', 'A', InputOption::VALUE_NONE, 'Do not ask for download, extract and database-update, just do it (if not --check-only is set)') @@ -63,9 +69,13 @@ final class UpdateCommand extends CliCommand $output->writeln('' . lng('update.dbupdate_required') . ''); if ($input->getOption('check-only')) { $output->writeln('Doing nothing because of "check-only" flag.'); + $this->askUpdateOptions($input, $output, null, false); } else { $yestoall = $input->getOption('yes-to-all') !== false; $helper = $this->getHelper('question'); + + $this->askUpdateOptions($input, $output, $helper, $yestoall); + $question = new ConfirmationQuestion('Update database? [no] ', false, '/^(y|j)/i'); if ($yestoall || $helper->ask($input, $output, $question)) { $result = $this->runUpdate($output, true); @@ -101,7 +111,7 @@ final class UpdateCommand extends CliCommand } // there is a new version if ($input->getOption('check-only')) { - $text = lng('update.uc_newinfo', [(Settings::Get('system.update_channel') != 'stable' ? Settings::Get('system.update_channel').' ' : ''), AutoUpdate::getFromResult('version'), Froxlor::VERSION]); + $text = lng('update.uc_newinfo', [(Settings::Get('system.update_channel') != 'stable' ? Settings::Get('system.update_channel') . ' ' : ''), AutoUpdate::getFromResult('version'), Froxlor::VERSION]); } else { $text = lng('admin.newerversionavailable') . ' ' . lng('admin.newerversiondetails', [AutoUpdate::getFromResult('version'), Froxlor::VERSION]); } @@ -152,6 +162,7 @@ final class UpdateCommand extends CliCommand // check whether we only wanted to check if ($input->getOption('check-only')) { //$output->writeln('Not proceeding as "check-only" is specified'); + $this->askUpdateOptions($input, $output, null, false); return $result; } else { $yestoall = $input->getOption('yes-to-all') !== false; @@ -174,6 +185,9 @@ final class UpdateCommand extends CliCommand if ($auex == 0) { $output->writeln("Froxlor files updated successfully."); $result = self::SUCCESS; + + $this->askUpdateOptions($input, $output, $helper, $yestoall); + $question = new ConfirmationQuestion('Update database? [no] ', false, '/^(y|j)/i'); if ($yestoall || $helper->ask($input, $output, $question)) { $result = $this->runUpdate($output, true); @@ -195,12 +209,141 @@ final class UpdateCommand extends CliCommand return $result; } + /** + * @param InputInterface $input + * @param OutputInterface $output + * @param $helper + * @param bool $yestoall + * @return void + */ + private function askUpdateOptions(InputInterface $input, OutputInterface $output, $helper, bool $yestoall = false) + { + // check for preconfigs + $preconfig = Preconfig::getPreConfig(true); + $show_options_only = $input->getOption('show-update-options') !== false; + if (!is_null($helper) && $show_options_only) { + $output->writeln('Unsetting "show-update-options" due to not being called with "check-only".'); + $show_options_only = false; + } + $update_options = []; + // set parameters + $uOptions = $input->getOption('update-options'); + if (!empty($uOptions)) { + $options_value = []; + foreach ($uOptions as $givenOption) { + $optVal = explode("=", $givenOption); + if (count($optVal) == 2) { + $options_value[$optVal[0]] = $optVal[1]; + } + } + } + if (!empty($preconfig)) { + krsort($preconfig); + foreach ($preconfig as $section) { + if (!$show_options_only) { + $output->writeln("Updater questions for " . $section['title'] . ""); + } + foreach ($section['fields'] as $update_field => $metainfo) { + if (isset($options_value[$update_field])) { + $output->writeln('Setting given parameter "' . $update_field . '" to "' . $options_value[$update_field] . '"'); + $_POST[$update_field] = $options_value[$update_field]; + continue; + } + $default = null; + $question_text = html_entity_decode(strip_tags($metainfo['label']), ENT_QUOTES | ENT_IGNORE, "UTF-8"); + if ($metainfo['type'] == 'checkbox') { + $default = (int)$metainfo['checked']; + if ($show_options_only) { + $update_options[] = [ + 'name' => $update_field, + 'question' => $question_text, + 'default' => $default, + 'choices' => '0: No' . PHP_EOL . '1: Yes' . PHP_EOL + ]; + } else { + $question = new ConfirmationQuestion($question_text . ' [' . ($metainfo['checked'] ? 'yes' : 'no') . '] ', (bool)$metainfo['checked'], '/^(y|j)/i'); + } + } elseif ($metainfo['type'] == 'select') { + $default = $metainfo['selected']; + $choices = ""; + foreach (array_values($metainfo['select_var'] ?? []) as $index => $choice) { + $choices .= $index . ': ' . $choice . PHP_EOL; + } + if ($show_options_only) { + $update_options[] = [ + 'name' => $update_field, + 'question' => $question_text, + 'default' => !empty($default) ? $default : '-', + 'choices' => $choices + ]; + } else { + $question = new ChoiceQuestion( + $question_text, + array_values($metainfo['select_var'] ?? []), + $metainfo['selected'] + ); + $question->setValidator(function ($answer) use ($metainfo): string { + $key = array_keys($metainfo['select_var'])[(int)$answer] ?? false; // Find the key based on the selected value + if ($key === false) { + throw new \RuntimeException('Invalid selection.'); + } + return $key; + }); + } + } elseif ($metainfo['type'] == 'text') { + $default = $metainfo['value'] ?? ''; + if ($show_options_only) { + $update_options[] = [ + 'name' => $update_field, + 'question' => $question_text, + 'default' => $default, + 'choices' => PHP_EOL + ]; + } else { + $question = new Question($question_text . (!empty($metainfo['value']) ? ' [' . $metainfo['value'] . ']' : ''), $default); + $question->setValidator(function (string $answer) use ($metainfo): string { + if (($metainfo['mandatory'] ?? false) && empty($answer)) { + throw new \RuntimeException( + 'Answer cannot be empty' + ); + } + if (!empty($metainfo['pattern'] ?? "") && !preg_match("/" . $metainfo['pattern'] . "/", $answer)) { + throw new \RuntimeException('Answer does not seem to be in valid format'); + } + return $answer; + }); + } + } else { + $output->writeln("Unknown type " . $metainfo['type'] . ""); + continue; + } + + if (!$show_options_only) { + if ($yestoall) { + $_POST[$update_field] = $default; + } else { + $_POST[$update_field] = $helper->ask($input, $output, $question); + } + } + } + } + + if ($show_options_only) { + $io = new SymfonyStyle($input, $output); + $io->table( + ['Parameter', 'Description', 'Default', 'Choices'], + $update_options + ); + } + } + } + private function mailNotify(InputInterface $input, OutputInterface $output) { if ($input->getOption('mail-notify')) { $last_check_version = Settings::Get('system.update_notify_last'); if (Update::versionInUpdate($last_check_version, AutoUpdate::getFromResult('version'))) { - $text = lng('update.uc_newinfo', [(Settings::Get('system.update_channel') != 'stable' ? Settings::Get('system.update_channel').' ' : ''), AutoUpdate::getFromResult('version'), Froxlor::VERSION]); + $text = lng('update.uc_newinfo', [(Settings::Get('system.update_channel') != 'stable' ? Settings::Get('system.update_channel') . ' ' : ''), AutoUpdate::getFromResult('version'), Froxlor::VERSION]); $mail = new Mailer(true); $mail->Body = $text; $mail->Subject = "[froxlor] " . lng('update.notify_subject'); diff --git a/lib/Froxlor/Database/Manager/DbManagerMySQL.php b/lib/Froxlor/Database/Manager/DbManagerMySQL.php index 7ba1a43d..481c4dc3 100644 --- a/lib/Froxlor/Database/Manager/DbManagerMySQL.php +++ b/lib/Froxlor/Database/Manager/DbManagerMySQL.php @@ -187,21 +187,23 @@ class DbManagerMySQL */ public function deleteUser(string $username, string $host) { - if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.0.2', '<')) { - // Revoke privileges (only required for MySQL 4.1.2 - 5.0.1) - $stmt = Database::prepare("REVOKE ALL PRIVILEGES ON * . * FROM `" . $username . "`@`" . $host . "`"); - Database::pexecute($stmt); + if ($this->userExistsOnHost($username, $host)) { + if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.0.2', '<')) { + // Revoke privileges (only required for MySQL 4.1.2 - 5.0.1) + $stmt = Database::prepare("REVOKE ALL PRIVILEGES ON * . * FROM `" . $username . "`@`" . $host . "`"); + Database::pexecute($stmt); + } + // as of MySQL 5.0.2 this also revokes privileges. (requires MySQL 4.1.2+) + if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.7.0', '<')) { + $stmt = Database::prepare("DROP USER :username@:host"); + } else { + $stmt = Database::prepare("DROP USER IF EXISTS :username@:host"); + } + Database::pexecute($stmt, [ + "username" => $username, + "host" => $host + ]); } - // as of MySQL 5.0.2 this also revokes privileges. (requires MySQL 4.1.2+) - if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.7.0', '<')) { - $stmt = Database::prepare("DROP USER :username@:host"); - } else { - $stmt = Database::prepare("DROP USER IF EXISTS :username@:host"); - } - Database::pexecute($stmt, [ - "username" => $username, - "host" => $host - ]); } /** diff --git a/lib/Froxlor/Install/Preconfig.php b/lib/Froxlor/Install/Preconfig.php index 43e9942e..86083c12 100644 --- a/lib/Froxlor/Install/Preconfig.php +++ b/lib/Froxlor/Install/Preconfig.php @@ -85,27 +85,31 @@ class Preconfig } } } + /** * Function getPreConfig * * outputs various form-field-arrays before the update process * can be continued (asks for agreement whatever is being asked) * + * @param bool $no_check * @return array */ - public static function getPreConfig(): array + public static function getPreConfig(bool $no_check = false): array { $preconfig = new self(); if ($preconfig->hasPreConfig()) { - $agree = [ - 'title' => 'Check', - 'fields' => [ - 'update_changesagreed' => ['mandatory' => true, 'type' => 'checkrequired', 'value' => 1, 'label' => 'I have read the update notifications above and I am aware of the changes made to my system.'], - 'update_preconfig' => ['type' => 'hidden', 'value' => 1] - ] - ]; - $preconfig->addToPreConfig($agree); + if (!$no_check) { + $agree = [ + 'title' => 'Check', + 'fields' => [ + 'update_changesagreed' => ['mandatory' => true, 'type' => 'checkrequired', 'value' => 1, 'label' => 'I have read the update notifications above and I am aware of the changes made to my system.'], + 'update_preconfig' => ['type' => 'hidden', 'value' => 1] + ] + ]; + $preconfig->addToPreConfig($agree); + } return $preconfig->getData(); } return [];