diff --git a/bin/froxlor-cli b/bin/froxlor-cli index 29753950..664f6be8 100755 --- a/bin/froxlor-cli +++ b/bin/froxlor-cli @@ -27,6 +27,7 @@ declare(strict_types=1); use Symfony\Component\Console\Application; +use Froxlor\Cli\RunApiCommand; use Froxlor\Cli\ConfigServices; use Froxlor\Cli\PhpSessionclean; use Froxlor\Cli\SwitchServerIp; @@ -46,6 +47,7 @@ require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/lib/tables.inc.php'; $application = new Application('froxlor-cli', Froxlor::getFullVersion()); +$application->add(new RunApiCommand()); $application->add(new ConfigServices()); $application->add(new PhpSessionclean()); $application->add(new SwitchServerIp()); diff --git a/lib/Froxlor/Ajax/Ajax.php b/lib/Froxlor/Ajax/Ajax.php index 200e64d5..d6f8b3ab 100644 --- a/lib/Froxlor/Ajax/Ajax.php +++ b/lib/Froxlor/Ajax/Ajax.php @@ -80,6 +80,8 @@ class Ajax return $this->editApiKey(); case 'getConfigDetails': return $this->getConfigDetails(); + case 'getConfigJsonExport': + return $this->getConfigJsonExport(); default: return $this->errorResponse('Action not found!'); } @@ -324,4 +326,19 @@ class Ajax } return $this->errorResponse('Not allowed', 403); } + + /** + * download JSON export of config-selection + */ + private function getConfigJsonExport() + { + if (isset($this->userinfo['adminsession']) && $this->userinfo['adminsession'] == 1 && $this->userinfo['change_serversettings'] == 1) { + $params = $_GET; + unset($params['action']); + unset($params['finish']); + header('Content-disposition: attachment; filename=froxlor-config-' . time() . '.json'); + return $this->jsonResponse($params); + } + return $this->errorResponse('Not allowed', 403); + } } diff --git a/lib/Froxlor/Api/ApiParameter.php b/lib/Froxlor/Api/ApiParameter.php index 72f035cd..6fbb1643 100644 --- a/lib/Froxlor/Api/ApiParameter.php +++ b/lib/Froxlor/Api/ApiParameter.php @@ -61,6 +61,12 @@ abstract class ApiParameter */ private function trimArray($input) { + if ($input === '') { + return ""; + } + if (is_numeric($input) || is_null($input)) { + return $input; + } if (!is_array($input)) { return trim($input); } diff --git a/lib/Froxlor/Api/Commands/Admins.php b/lib/Froxlor/Api/Commands/Admins.php index 58c74f14..044933db 100644 --- a/lib/Froxlor/Api/Commands/Admins.php +++ b/lib/Froxlor/Api/Commands/Admins.php @@ -582,7 +582,7 @@ class Admins extends ApiCommand implements ResourceEntity $idna_convert = new IdnaWrapper(); $email = $idna_convert->encode(Validate::validate($email, 'email', '', '', [], true)); $def_language = Validate::validate($def_language, 'default language', '', '', [], true); - $custom_notes = Validate::validate(str_replace("\r\n", "\n", $custom_notes), 'custom_notes', Validate::REGEX_CONF_TEXT, '', [], true); + $custom_notes = Validate::validate(str_replace("\r\n", "\n", $custom_notes ?? ""), 'custom_notes', Validate::REGEX_CONF_TEXT, '', [], true); $theme = Validate::validate($theme, 'theme', '', '', [], true); $password = Validate::validate($password, 'password', '', '', [], true); diff --git a/lib/Froxlor/Api/Commands/Domains.php b/lib/Froxlor/Api/Commands/Domains.php index 4355de44..87a1d302 100644 --- a/lib/Froxlor/Api/Commands/Domains.php +++ b/lib/Froxlor/Api/Commands/Domains.php @@ -1451,6 +1451,7 @@ class Domains extends ApiCommand implements ResourceEntity $ipandports = $this->validateIpAddresses($p_ipandports, false, $result['id']); // check ssl IP if (empty($p_ssl_ipandports) || (!is_array($p_ssl_ipandports) && is_null($p_ssl_ipandports))) { + $p_ssl_ipandports = []; foreach ($result['ipsandports'] as $ip) { if ($ip['ssl'] == 1) { $p_ssl_ipandports[] = $ip['id']; diff --git a/lib/Froxlor/Api/Commands/EmailForwarders.php b/lib/Froxlor/Api/Commands/EmailForwarders.php index 61222cad..a190ee1d 100644 --- a/lib/Froxlor/Api/Commands/EmailForwarders.php +++ b/lib/Froxlor/Api/Commands/EmailForwarders.php @@ -84,7 +84,7 @@ class EmailForwarders extends ApiCommand implements ResourceEntity $id = $result['id']; // current destination array - $result['destination_array'] = explode(' ', $result['destination']); + $result['destination_array'] = explode(' ', ($result['destination'] ?? "")); // prepare destination $destination = trim($destination); diff --git a/lib/Froxlor/Api/Commands/IpsAndPorts.php b/lib/Froxlor/Api/Commands/IpsAndPorts.php index 212dec0e..5c2a25f6 100644 --- a/lib/Froxlor/Api/Commands/IpsAndPorts.php +++ b/lib/Froxlor/Api/Commands/IpsAndPorts.php @@ -166,9 +166,11 @@ class IpsAndPorts extends ApiCommand implements ResourceEntity $listen_statement = !empty($this->getBoolParam('listen_statement', true, 0)) ? 1 : 0; $namevirtualhost_statement = !empty($this->getBoolParam('namevirtualhost_statement', true, 0)) ? 1 : 0; $vhostcontainer = !empty($this->getBoolParam('vhostcontainer', true, 0)) ? 1 : 0; - $specialsettings = Validate::validate(str_replace("\r\n", "\n", $this->getParam('specialsettings', true, '')), 'specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); + $ss = $this->getParam('specialsettings', true, ''); + $specialsettings = Validate::validate(str_replace("\r\n", "\n", $ss ?? ""), 'specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); $vhostcontainer_servername_statement = !empty($this->getBoolParam('vhostcontainer_servername_statement', true, 1)) ? 1 : 0; - $default_vhostconf_domain = Validate::validate(str_replace("\r\n", "\n", $this->getParam('default_vhostconf_domain', true, '')), 'default_vhostconf_domain', Validate::REGEX_CONF_TEXT, '', [], true); + $dvd = $this->getParam('default_vhostconf_domain', true, ''); + $default_vhostconf_domain = Validate::validate(str_replace("\r\n", "\n", $dvd), 'default_vhostconf_domain', Validate::REGEX_CONF_TEXT, '', [], true); $docroot = Validate::validate($this->getParam('docroot', true, ''), 'docroot', Validate::REGEX_DIR, '', [], true); if ((int)Settings::Get('system.use_ssl') == 1) { @@ -177,9 +179,11 @@ class IpsAndPorts extends ApiCommand implements ResourceEntity $ssl_key_file = Validate::validate($this->getParam('ssl_key_file', $ssl, ''), 'ssl_key_file', '', '', [], true); $ssl_ca_file = Validate::validate($this->getParam('ssl_ca_file', true, ''), 'ssl_ca_file', '', '', [], true); $ssl_cert_chainfile = Validate::validate($this->getParam('ssl_cert_chainfile', true, ''), 'ssl_cert_chainfile', '', '', [], true); - $ssl_specialsettings = Validate::validate(str_replace("\r\n", "\n", $this->getParam('ssl_specialsettings', true, '')), 'ssl_specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); + $sslss = $this->getParam('ssl_specialsettings', true, ''); + $ssl_specialsettings = Validate::validate(str_replace("\r\n", "\n", $sslss ?? ""), 'ssl_specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); $include_specialsettings = !empty($this->getBoolParam('include_specialsettings', true, 0)) ? 1 : 0; - $ssl_default_vhostconf_domain = Validate::validate(str_replace("\r\n", "\n", $this->getParam('ssl_default_vhostconf_domain', true, '')), 'ssl_default_vhostconf_domain', Validate::REGEX_CONF_TEXT, '', [], true); + $ssldvd = $this->getParam('ssl_default_vhostconf_domain', true, ''); + $ssl_default_vhostconf_domain = Validate::validate(str_replace("\r\n", "\n", $ssldvd ?? ""), 'ssl_default_vhostconf_domain', Validate::REGEX_CONF_TEXT, '', [], true); $include_default_vhostconf_domain = !empty($this->getBoolParam('include_default_vhostconf_domain', true, 0)) ? 1 : 0; } else { $ssl = 0; @@ -401,9 +405,11 @@ class IpsAndPorts extends ApiCommand implements ResourceEntity $listen_statement = $this->getBoolParam('listen_statement', true, $result['listen_statement']); $namevirtualhost_statement = $this->getBoolParam('namevirtualhost_statement', true, $result['namevirtualhost_statement']); $vhostcontainer = $this->getBoolParam('vhostcontainer', true, $result['vhostcontainer']); - $specialsettings = Validate::validate(str_replace("\r\n", "\n", $this->getParam('specialsettings', true, $result['specialsettings'])), 'specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); + $ss = $this->getParam('specialsettings', true, $result['specialsettings']); + $specialsettings = Validate::validate(str_replace("\r\n", "\n", $ss ?? ""), 'specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); $vhostcontainer_servername_statement = $this->getParam('vhostcontainer_servername_statement', true, $result['vhostcontainer_servername_statement']); - $default_vhostconf_domain = Validate::validate(str_replace("\r\n", "\n", $this->getParam('default_vhostconf_domain', true, $result['default_vhostconf_domain'])), 'default_vhostconf_domain', Validate::REGEX_CONF_TEXT, '', [], true); + $dvd = $this->getParam('default_vhostconf_domain', true, $result['default_vhostconf_domain']); + $default_vhostconf_domain = Validate::validate(str_replace("\r\n", "\n", $dvd ?? ""), 'default_vhostconf_domain', Validate::REGEX_CONF_TEXT, '', [], true); $docroot = Validate::validate($this->getParam('docroot', true, $result['docroot']), 'docroot', Validate::REGEX_DIR, '', [], true); if ((int)Settings::Get('system.use_ssl') == 1) { @@ -412,9 +418,11 @@ class IpsAndPorts extends ApiCommand implements ResourceEntity $ssl_key_file = Validate::validate($this->getParam('ssl_key_file', $ssl, $result['ssl_key_file']), 'ssl_key_file', '', '', [], true); $ssl_ca_file = Validate::validate($this->getParam('ssl_ca_file', true, $result['ssl_ca_file']), 'ssl_ca_file', '', '', [], true); $ssl_cert_chainfile = Validate::validate($this->getParam('ssl_cert_chainfile', true, $result['ssl_cert_chainfile']), 'ssl_cert_chainfile', '', '', [], true); - $ssl_specialsettings = Validate::validate(str_replace("\r\n", "\n", $this->getParam('ssl_specialsettings', true, $result['ssl_specialsettings'])), 'ssl_specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); + $sslss = $this->getParam('ssl_specialsettings', true, $result['ssl_specialsettings']); + $ssl_specialsettings = Validate::validate(str_replace("\r\n", "\n", $sslss ?? ""), 'ssl_specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); $include_specialsettings = $this->getBoolParam('include_specialsettings', true, $result['include_specialsettings']); - $ssl_default_vhostconf_domain = Validate::validate(str_replace("\r\n", "\n", $this->getParam('ssl_default_vhostconf_domain', true, $result['ssl_default_vhostconf_domain'])), 'ssl_default_vhostconf_domain', Validate::REGEX_CONF_TEXT, '', [], true); + $ssldvd = $this->getParam('ssl_default_vhostconf_domain', true, $result['ssl_default_vhostconf_domain']); + $ssl_default_vhostconf_domain = Validate::validate(str_replace("\r\n", "\n", $ssldvd ?? ""), 'ssl_default_vhostconf_domain', Validate::REGEX_CONF_TEXT, '', [], true); $include_default_vhostconf_domain = $this->getBoolParam('include_default_vhostconf_domain', true, $result['include_default_vhostconf_domain']); } else { $ssl = 0; diff --git a/lib/Froxlor/Cli/RunApiCommand.php b/lib/Froxlor/Cli/RunApiCommand.php new file mode 100644 index 00000000..5dd8b95e --- /dev/null +++ b/lib/Froxlor/Cli/RunApiCommand.php @@ -0,0 +1,142 @@ + + * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 + */ + +namespace Froxlor\Cli; + +use Exception; +use PDO; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; +use Froxlor\Database\Database; + +final class RunApiCommand extends CliCommand +{ + + protected function configure() + { + $this->setName('froxlor:api-call'); + $this->setDescription('Run an API command as given user'); + $this->addArgument('user', InputArgument::REQUIRED, 'Loginname of the user you want to run the command as') + ->addArgument('api-command', InputArgument::REQUIRED, 'The command to execute in the form "Module.function"') + ->addArgument('parameters', InputArgument::OPTIONAL, 'Paramaters to pass to the command as JSON array'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $result = self::SUCCESS; + + $result = $this->validateRequirements($input, $output); + + try { + $loginname = $input->getArgument('user'); + $userinfo = $this->getUserByName($loginname); + $command = $input->getArgument('api-command'); + $apicmd = $this->validateCommand($command); + $module = "\\Froxlor\\Api\\Commands\\" . $apicmd['class']; + $function = $apicmd['function']; + $params_json = $input->getArgument('parameters'); + $params = json_decode($params_json ?? '', true); + $json_result = $module::getLocal($userinfo, $params)->{$function}(); + $output->write($json_result); + $result = self::SUCCESS; + } catch (Exception $e) { + $output->writeln('' . $e->getMessage() . ''); + $result = self::FAILURE; + } + + return $result; + } + + private function validateCommand(string $command): array + { + $command = explode(".", $command); + + if (count($command) != 2) { + throw new Exception("The given command is invalid."); + } + // simply check for file-existance, as we do not want to use our autoloader because this way + // it will recognize non-api classes+methods as valid commands + $apiclass = '\\Froxlor\\Api\\Commands\\' . $command[0]; + if (!class_exists($apiclass) || !@method_exists($apiclass, $command[1])) { + throw new Exception("Unknown command"); + } + return ['class' => $command[0], 'function' => $command[1]]; + } + + private function getUserByName(?string $loginname): array + { + if (empty($loginname)) { + throw new Exception("Empty username"); + } + + $stmt = Database::prepare(" + SELECT `loginname` AS `customer` + FROM `" . TABLE_PANEL_CUSTOMERS . "` + WHERE `loginname`= :loginname + "); + Database::pexecute($stmt, [ + "loginname" => $loginname + ]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($row && $row['customer'] == $loginname) { + $table = "`" . TABLE_PANEL_CUSTOMERS . "`"; + $adminsession = '0'; + } else { + $stmt = Database::prepare(" + SELECT `loginname` AS `admin` FROM `" . TABLE_PANEL_ADMINS . "` + WHERE `loginname`= :loginname + "); + Database::pexecute($stmt, [ + "loginname" => $loginname + ]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($row && $row['admin'] == $loginname) { + $table = "`" . TABLE_PANEL_ADMINS . "`"; + $adminsession = '1'; + } else { + throw new Exception("Unknown user '" . $loginname . "'"); + } + } + + $userinfo_stmt = Database::prepare(" + SELECT * FROM $table + WHERE `loginname`= :loginname + "); + Database::pexecute($userinfo_stmt, [ + "loginname" => $loginname + ]); + $userinfo = $userinfo_stmt->fetch(PDO::FETCH_ASSOC); + $userinfo['adminsession'] = $adminsession; + + if ($userinfo['deactivated']) { + throw new Exception("User '" . $loginname . "' is currently deactivated"); + } + + return $userinfo; + } +} diff --git a/lib/Froxlor/Cron/TaskId.php b/lib/Froxlor/Cron/TaskId.php index 4d388ce2..d80e6c11 100644 --- a/lib/Froxlor/Cron/TaskId.php +++ b/lib/Froxlor/Cron/TaskId.php @@ -27,7 +27,7 @@ namespace Froxlor\Cron; use ReflectionClass; -class TaskId +final class TaskId { /** * TYPE=1 MEANS TO REBUILD APACHE VHOSTS.CONF diff --git a/lib/Froxlor/Database/Manager/DbManagerMySQL.php b/lib/Froxlor/Database/Manager/DbManagerMySQL.php index aff227fc..4808e4b5 100644 --- a/lib/Froxlor/Database/Manager/DbManagerMySQL.php +++ b/lib/Froxlor/Database/Manager/DbManagerMySQL.php @@ -138,7 +138,7 @@ class DbManagerMySQL */ public function deleteDatabase($dbname = null) { - if (Database::getAttribute(PDO::ATTR_SERVER_VERSION) < '5.0.2') { + if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.0.2', '<')) { // failsafe if user has been deleted manually (requires MySQL 4.1.2+) $stmt = Database::prepare("REVOKE ALL PRIVILEGES, GRANT OPTION FROM `" . $dbname . "`"); Database::pexecute($stmt, [], false); diff --git a/templates/Froxlor/settings/configuration.html.twig b/templates/Froxlor/settings/configuration.html.twig index e8e5cc5a..2d29a5d6 100644 --- a/templates/Froxlor/settings/configuration.html.twig +++ b/templates/Froxlor/settings/configuration.html.twig @@ -104,7 +104,9 @@ {{ lng('admin.configfiles.recommendednote') }}
+ +
diff --git a/templates/Froxlor/src/js/components/configfiles.js b/templates/Froxlor/src/js/components/configfiles.js index 6debff0b..4ab51b03 100644 --- a/templates/Froxlor/src/js/components/configfiles.js +++ b/templates/Froxlor/src/js/components/configfiles.js @@ -12,6 +12,18 @@ $(function () { }) }); + /* + * export/download JSON file (e.g. for usage with config-services) + */ + $('#downloadSelectionAsJson').on('click', function () { + var formData = $(this).closest('form').serialize(); + window.location = "lib/ajax.php?action=getConfigJsonExport&" + formData; + }); + + /* + * open modal window to show selected config-commands/files + * for selected daemon + */ $('.show-config').on('click', function () { var distro = $(this).data('dist'); var section = $(this).data('section'); diff --git a/tests/Cron/TaskIdTest.php b/tests/Cron/TaskIdTest.php index 397ffe87..9b24eeec 100644 --- a/tests/Cron/TaskIdTest.php +++ b/tests/Cron/TaskIdTest.php @@ -48,19 +48,6 @@ class TaskIDTest extends TestCase $this->assertFalse($isNegativeValid, "Negative task should be invalid"); } - public function testAcceptNewTaskId() - { - $isTESTTASKValid = TaskIdExtended::isValid(10101010); - $this->assertTrue($isTESTTASKValid); - } - - - public function testFixedTaskIdTable() - { - $isTESTTASKValid = TaskIdExtended::isValid(10101010); - $this->assertTrue($isTESTTASKValid); - } - public function testIdMappingCorrect() { foreach($this->fixedids as $name => $expected) { $result = constant("\Froxlor\Cron\TaskId::$name"); @@ -78,7 +65,3 @@ class TaskIDTest extends TestCase $this->assertFalse($unknownIDResult); } } - -class TaskIdExtended extends TaskId { - const TESTTASK = 10101010; -}