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 <d00p@froxlor.org>
This commit is contained in:
Michael Kaufmann
2024-10-02 15:27:42 +02:00
parent 228eb244fa
commit f31c032508
4 changed files with 175 additions and 26 deletions

View File

@@ -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('<info>' . lng('update.dbupdate_required') . '</>');
if ($input->getOption('check-only')) {
$output->writeln('<comment>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('<comment>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("<info>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('<comment>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("<info>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("<error>Unknown type " . $metainfo['type'] . "</error>");
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');

View File

@@ -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
]);
}
/**

View File

@@ -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' => '<strong>I have read the update notifications above and I am aware of the changes made to my system.</strong>'],
'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' => '<strong>I have read the update notifications above and I am aware of the changes made to my system.</strong>'],
'update_preconfig' => ['type' => 'hidden', 'value' => 1]
]
];
$preconfig->addToPreConfig($agree);
}
return $preconfig->getData();
}
return [];