diff --git a/admin_mysqlserver.php b/admin_mysqlserver.php new file mode 100644 index 00000000..5d0d1db3 --- /dev/null +++ b/admin_mysqlserver.php @@ -0,0 +1,144 @@ + + * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 + */ + +const AREA = 'admin'; +require __DIR__ . '/lib/init.php'; + +use Froxlor\Api\Commands\MysqlServer; +use Froxlor\FroxlorLogger; +use Froxlor\PhpHelper; +use Froxlor\UI\Collection; +use Froxlor\UI\HTML; +use Froxlor\UI\Listing; +use Froxlor\UI\Panel\UI; +use Froxlor\UI\Request; +use Froxlor\UI\Response; + +$id = (int)Request::get('id'); + +if ($page == 'mysqlserver' || $page == 'overview') { + if ($action == '') { + $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "viewed admin_mysqlserver"); + + try { + $mysqlserver_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.mysqlserver.php'; + $collection = (new Collection(MysqlServer::class, $userinfo)) + ->withPagination($mysqlserver_list_data['mysqlserver_list']['columns']); + } catch (Exception $e) { + Response::dynamicError($e->getMessage()); + } + + UI::view('user/table.html.twig', [ + 'listing' => Listing::format($collection, $mysqlserver_list_data, 'mysqlserver_list'), + 'actions_links' => [ + [ + 'href' => $linker->getLink(['section' => 'mysqlserver', 'page' => $page, 'action' => 'add']), + 'label' => lng('admin.mysqlserver.add') + ] + ] + ]); + } elseif ($action == 'delete' && $id != 0) { + try { + $json_result = MysqlServer::getLocal($userinfo, [ + 'id' => $id + ])->get(); + } catch (Exception $e) { + Response::dynamicError($e->getMessage()); + } + $result = json_decode($json_result, true)['data']; + + if (isset($result['id']) && $result['id'] == $id) { + if (isset($_POST['send']) && $_POST['send'] == 'send') { + try { + MysqlServer::getLocal($userinfo, [ + 'id' => $id + ])->delete(); + } catch (Exception $e) { + Response::dynamicError($e->getMessage()); + } + + Response::redirectTo($filename, [ + 'page' => $page + ]); + } else { + HTML::askYesNo('admin_mysqlserver_reallydelete', $filename, [ + 'id' => $id, + 'page' => $page, + 'action' => $action + ], $result['caption'] . ' (' . $result['host'] . ')'); + } + } + } elseif ($action == 'add') { + if (isset($_POST['send']) && $_POST['send'] == 'send') { + try { + MysqlServer::getLocal($userinfo, $_POST)->add(); + } catch (Exception $e) { + Response::dynamicError($e->getMessage()); + } + Response::redirectTo($filename, [ + 'page' => $page + ]); + } else { + $mysqlserver_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/mysqlserver/formfield.mysqlserver_add.php'; + + UI::view('user/form.html.twig', [ + 'formaction' => $linker->getLink(['section' => 'mysqlserver']), + 'formdata' => $mysqlserver_add_data['mysqlserver_add'] + ]); + } + } elseif ($action == 'edit' && $id >= 0) { + try { + $json_result = MysqlServer::getLocal($userinfo, [ + 'id' => $id + ])->get(); + } catch (Exception $e) { + Response::dynamicError($e->getMessage()); + } + $result = json_decode($json_result, true)['data']; + + if (isset($result['id']) && $result['id'] == $id) { + if (isset($_POST['send']) && $_POST['send'] == 'send') { + try { + MysqlServer::getLocal($userinfo, $_POST)->update(); + } catch (Exception $e) { + Response::dynamicError($e->getMessage()); + } + Response::redirectTo($filename, [ + 'page' => $page + ]); + } else { + $result = PhpHelper::htmlentitiesArray($result); + + $mysqlserver_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/mysqlserver/formfield.mysqlserver_edit.php'; + + UI::view('user/form.html.twig', [ + 'formaction' => $linker->getLink(['section' => 'mysqlserver', 'id' => $id]), + 'formdata' => $mysqlserver_edit_data['mysqlserver_edit'], + 'editid' => $id + ]); + } + } + } +} diff --git a/customer_mysql.php b/customer_mysql.php index 6b91714d..44c74ae1 100644 --- a/customer_mysql.php +++ b/customer_mysql.php @@ -55,9 +55,7 @@ if ($page == 'overview' || $page == 'mysqls') { if ($action == '') { $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "viewed customer_mysql::mysqls"); - $dbservers_stmt = Database::query("SELECT COUNT(DISTINCT `dbserver`) as numservers FROM `" . TABLE_PANEL_DATABASES . "`"); - $dbserver = $dbservers_stmt->fetch(PDO::FETCH_ASSOC); - $count_mysqlservers = $dbserver['numservers']; + $multiple_mysqlservers = count(json_decode($userinfo['allowed_mysqlserver'] ?? '[]', true)) > 1; try { $mysql_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.mysqls.php'; diff --git a/lib/Froxlor/Api/Commands/MysqlServer.php b/lib/Froxlor/Api/Commands/MysqlServer.php index 24bcfd3e..cd2289f8 100644 --- a/lib/Froxlor/Api/Commands/MysqlServer.php +++ b/lib/Froxlor/Api/Commands/MysqlServer.php @@ -91,12 +91,12 @@ class MysqlServer extends ApiCommand implements ResourceEntity $test_connection = $this->getParam('test_connection', true, 1); // validation - $mysql_host = Validate::validate_ip2($mysql_host, true, 'invalidip', true, true, false); - if ($mysql_host === false) { - $mysql_host = Validate::validateLocalHostname($mysql_host); - if ($mysql_host === false) { - $mysql_host = Validate::validateDomain($mysql_host); - if ($mysql_host === false) { + $mysql_host_chk = Validate::validate_ip2($mysql_host, true, 'invalidip', true, true, false); + if ($mysql_host_chk === false) { + $mysql_host_chk = Validate::validateLocalHostname($mysql_host); + if ($mysql_host_chk === false) { + $mysql_host_chk = Validate::validateDomain($mysql_host); + if ($mysql_host_chk === false) { throw new Exception("Invalid mysql server ip/hostname", 406); } } @@ -170,8 +170,10 @@ class MysqlServer extends ApiCommand implements ResourceEntity /** * remove a mysql-server * + * @param int $id + * optional the number of the mysql server (either id or dbserver must be set) * @param int $dbserver - * the number of the mysql-server + * optional the number of the mysql server (either id or dbserver must be set) * * @access admin * @throws Exception @@ -181,7 +183,10 @@ class MysqlServer extends ApiCommand implements ResourceEntity { $this->validateAccess(); - $dbserver = (int) $this->getParam('dbserver'); + $id = (int) $this->getParam('id', true, -1); + $dn_optional = $id >= 0; + $dbserver = (int) $this->getParam('dbserver', $dn_optional, -1); + $dbserver = $id >= 0 ? $id : $dbserver; if ($dbserver == 0) { throw new Exception('Cannot delete first/default mysql-server', 406); @@ -197,7 +202,7 @@ class MysqlServer extends ApiCommand implements ResourceEntity // check whether the server is in use by any customer $result_ms = $this->databasesOnServer(true, $dbserver); if ($result_ms > 0) { - Response::standardError('mysqlserverstillhasdbs', '', true); + throw new Exception(lng('error.mysqlserverstillhasdbs'), 406); } // when removing, remove from list of allowed_mysqlservers from any customers @@ -239,6 +244,7 @@ class MysqlServer extends ApiCommand implements ResourceEntity } // passwords will not be returned in any case for security reasons unset($sqlrootdata['password']); + $sqlrootdata['id'] = $index; $result[$index] = $sqlrootdata; } @@ -268,8 +274,10 @@ class MysqlServer extends ApiCommand implements ResourceEntity /** * Return info about a specific mysql-server * + * @param int $id + * optional the number of the mysql server (either id or dbserver must be set) * @param int $dbserver - * the number of the mysql-server + * optional the number of the mysql server (either id or dbserver must be set) * * @access admin, customer * @throws Exception @@ -277,7 +285,10 @@ class MysqlServer extends ApiCommand implements ResourceEntity */ public function get() { - $dbserver = (int) $this->getParam('dbserver'); + $id = (int) $this->getParam('id', true, -1); + $dn_optional = $id >= 0; + $dbserver = (int) $this->getParam('dbserver', $dn_optional, -1); + $dbserver = $id >= 0 ? $id : $dbserver; // get all data from lib/userdata require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; @@ -297,14 +308,17 @@ class MysqlServer extends ApiCommand implements ResourceEntity } unset($sql_root[$dbserver]['password']); + $sql_root[$dbserver]['id'] = $dbserver; return $this->response($sql_root[$dbserver]); } /** * update given mysql-server * + * @param int $id + * optional the number of the mysql server (either id or dbserver must be set) * @param int $dbserver - * the number of the mysql server + * optional the number of the mysql server (either id or dbserver must be set) * @param string $mysql_host * ip/hostname of mysql-server * @param string $mysql_port @@ -332,7 +346,10 @@ class MysqlServer extends ApiCommand implements ResourceEntity { $this->validateAccess(); - $dbserver = (int) $this->getParam('dbserver'); + $id = (int) $this->getParam('id', true, -1); + $dn_optional = $id >= 0; + $dbserver = (int) $this->getParam('dbserver', $dn_optional, -1); + $dbserver = $id >= 0 ? $id : $dbserver; require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; @@ -342,7 +359,11 @@ class MysqlServer extends ApiCommand implements ResourceEntity $result = $sql_root[$dbserver]; - $mysql_host = $this->getParam('mysql_host', true, $result['host']); + if ($dbserver == 0) { + $mysql_host = $result['host']; + } else { + $mysql_host = $this->getParam('mysql_host', true, $result['host']); + } $mysql_port = $this->getParam('mysql_port', true, $result['port'] ?? 3306); $mysql_ca = $this->getParam('mysql_ca', true, $result['ssl']['caFile'] ?? ''); $mysql_verifycert = $this->getBoolParam('mysql_verifycert', true, $result['ssl']['verifyServerCertificate'] ?? 0); @@ -353,12 +374,12 @@ class MysqlServer extends ApiCommand implements ResourceEntity $test_connection = $this->getParam('test_connection', true, 1); // validation - $mysql_host = Validate::validate_ip2($mysql_host, true, 'invalidip', true, true, false); - if ($mysql_host === false) { - $mysql_host = Validate::validateLocalHostname($mysql_host); - if ($mysql_host === false) { - $mysql_host = Validate::validateDomain($mysql_host); - if ($mysql_host === false) { + $mysql_host_chk = Validate::validate_ip2($mysql_host, true, 'invalidip', true, true, false); + if ($mysql_host_chk === false) { + $mysql_host_chk = Validate::validateLocalHostname($mysql_host); + if ($mysql_host_chk === false) { + $mysql_host_chk = Validate::validateDomain($mysql_host); + if ($mysql_host_chk === false) { throw new Exception("Invalid mysql server ip/hostname", 406); } } @@ -367,6 +388,14 @@ class MysqlServer extends ApiCommand implements ResourceEntity $privileged_password = Validate::validate($privileged_password, 'password', '', '', [], true); $description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true); + if ($mysql_host != $result['host']) { + // check whether the server is in use by any customer + $result_ms = $this->databasesOnServer(true, $dbserver); + if ($result_ms > 0) { + throw new Exception("Unable to update mysql-host as there are still databases on it", 406); + } + } + // testing connection with given credentials if ($test_connection) { $options = array( @@ -491,6 +520,12 @@ class MysqlServer extends ApiCommand implements ResourceEntity ['sql' => $sql, 'sql_root' => $sql_root], 'automatically generated userdata.inc.php for froxlor' ); + chmod(Froxlor::getInstallDir() . "/lib/userdata.inc.php", 0700); file_put_contents(Froxlor::getInstallDir() . "/lib/userdata.inc.php", $content); + chmod(Froxlor::getInstallDir() . "/lib/userdata.inc.php", 0400); + clearstatcache(); + if (function_exists('opcache_invalidate')) { + @opcache_invalidate(Froxlor::getInstallDir() . "/lib/userdata.inc.php", true); + } } } diff --git a/lib/formfields/admin/customer/formfield.customer_edit.php b/lib/formfields/admin/customer/formfield.customer_edit.php index 9a662ac0..e1aac3cf 100644 --- a/lib/formfields/admin/customer/formfield.customer_edit.php +++ b/lib/formfields/admin/customer/formfield.customer_edit.php @@ -273,7 +273,7 @@ return [ 'label' => lng('customer.mysqlserver'), 'type' => 'checkbox', 'values' => $mysql_servers, - 'value' => isset($result['allowed_mysqlserver']) && !empty($result['allowed_mysqlserver']) ? json_decode($result['allowed_phpconfigs'], JSON_OBJECT_AS_ARRAY) : [], + 'value' => isset($result['allowed_mysqlserver']) && !empty($result['allowed_mysqlserver']) ? json_decode($result['allowed_mysqlserver'], JSON_OBJECT_AS_ARRAY) : [], 'is_array' => 1 ], 'phpenabled' => [ diff --git a/lib/formfields/admin/mysqlserver/formfield.mysqlserver_add.php b/lib/formfields/admin/mysqlserver/formfield.mysqlserver_add.php new file mode 100644 index 00000000..d430d3f7 --- /dev/null +++ b/lib/formfields/admin/mysqlserver/formfield.mysqlserver_add.php @@ -0,0 +1,95 @@ + + * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 + */ + +use Froxlor\Settings; + +return [ + 'mysqlserver_add' => [ + 'title' => lng('admin.mysqlserver.add'), + 'image' => 'fa-solid fa-plus', + 'self_overview' => ['section' => 'mysqlserver', 'page' => 'mysqlserver'], + 'sections' => [ + 'section_a' => [ + 'title' => lng('admin.mysqlserver.mysqlserver'), + 'fields' => [ + 'mysql_host' => [ + 'label' => lng('admin.mysqlserver.host'), + 'type' => 'text', + 'mandatory' => true + ], + 'mysql_port' => [ + 'label' => lng('admin.mysqlserver.port'), + 'type' => 'number', + 'min' => 1, + 'max' => 65535, + 'value' => 3306, + 'mandatory' => true + ], + 'description' => [ + 'label' => lng('admin.mysqlserver.caption'), + 'type' => 'text', + ], + 'privileged_user' => [ + 'label' => lng('admin.mysqlserver.user'), + 'type' => 'text', + 'mandatory' => true + ], + 'privileged_password' => [ + 'label' => lng('admin.mysqlserver.password'), + 'type' => 'password', + 'mandatory' => true + ], + 'allow_all_customers' => [ + 'label' => lng('admin.mysqlserver.allowall'), + 'type' => 'checkbox', + 'value' => '1', + 'checked' => false + ], + 'test_connection' => [ + 'label' => lng('admin.mysqlserver.testconn'), + 'type' => 'checkbox', + 'value' => '1', + 'checked' => true + ] + ] + ], + 'section_b' => [ + 'title' => lng('admin.mysqlserver.ssl'), + 'fields' => [ + 'mysql_ca' => [ + 'label' => lng('admin.mysqlserver.ssl_cert_file'), + 'type' => 'text' + ], + 'mysql_verifycert' => [ + 'label' => lng('admin.mysqlserver.verify_ca'), + 'type' => 'checkbox', + 'value' => '1', + 'checked' => false + ] + ] + ] + ] + ] +]; diff --git a/lib/formfields/admin/mysqlserver/formfield.mysqlserver_edit.php b/lib/formfields/admin/mysqlserver/formfield.mysqlserver_edit.php new file mode 100644 index 00000000..4d3ca9a5 --- /dev/null +++ b/lib/formfields/admin/mysqlserver/formfield.mysqlserver_edit.php @@ -0,0 +1,97 @@ + + * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 + */ + + +return [ + 'mysqlserver_edit' => [ + 'title' => lng('admin.mysqlserver.edit'), + 'image' => 'fa-solid fa-pen', + 'self_overview' => ['section' => 'mysqlserver', 'page' => 'mysqlserver'], + 'sections' => [ + 'section_a' => [ + 'title' => lng('admin.mysqlserver.mysqlserver'), + 'fields' => [ + 'mysql_host' => [ + 'label' => lng('admin.mysqlserver.host'), + 'type' => empty($result['id']) ? 'label' : 'text', + 'value' => $result['host'], + 'mandatory' => true, + ], + 'mysql_port' => [ + 'label' => lng('admin.mysqlserver.port'), + 'type' => 'number', + 'min' => 1, + 'max' => 65535, + 'value' => $result['port'], + 'mandatory' => true + ], + 'description' => [ + 'label' => lng('admin.mysqlserver.caption'), + 'type' => 'text', + 'value' => $result['caption'], + ], + 'privileged_user' => [ + 'label' => lng('admin.mysqlserver.user'), + 'type' => 'text', + 'value' => $result['user'], + 'mandatory' => true + ], + 'privileged_password' => [ + 'label' => lng('admin.mysqlserver.password_emptynochange'), + 'type' => 'password' + ], + 'allow_all_customers' => [ + 'label' => lng('admin.mysqlserver.allowall'), + 'type' => 'checkbox', + 'value' => '1', + 'checked' => false + ], + 'test_connection' => [ + 'label' => lng('admin.mysqlserver.testconn'), + 'type' => 'checkbox', + 'value' => '1', + 'checked' => true + ] + ] + ], + 'section_b' => [ + 'title' => lng('admin.mysqlserver.ssl'), + 'fields' => [ + 'mysql_ca' => [ + 'label' => lng('admin.mysqlserver.ssl_cert_file'), + 'type' => 'text', + 'value' => $result['ssl']['caFile'] ?? "", + ], + 'mysql_verifycert' => [ + 'label' => lng('admin.mysqlserver.verify_ca'), + 'type' => 'checkbox', + 'value' => '1', + 'checked' => $result['ssl']['verifyServerCertificate'] ?? false, + ] + ] + ] + ] + ] +]; diff --git a/lib/formfields/admin/mysqlserver/index.html b/lib/formfields/admin/mysqlserver/index.html new file mode 100644 index 00000000..e69de29b diff --git a/lib/navigation/00.froxlor.main.php b/lib/navigation/00.froxlor.main.php index 49f4ea86..75e3b009 100644 --- a/lib/navigation/00.froxlor.main.php +++ b/lib/navigation/00.froxlor.main.php @@ -182,6 +182,10 @@ return [ 'label' => lng('admin.ipsandports.ipsandports'), 'required_resources' => 'change_serversettings' ], + [ + 'url' => 'admin_mysqlserver.php?page=mysqlserver', + 'label' => lng('admin.mysqlserver.mysqlserver'), + ], [ 'url' => 'admin_plans.php?page=overview', 'label' => lng('admin.plans.plans'), diff --git a/lib/tablelisting/admin/tablelisting.mysqlserver.php b/lib/tablelisting/admin/tablelisting.mysqlserver.php new file mode 100644 index 00000000..2f47953e --- /dev/null +++ b/lib/tablelisting/admin/tablelisting.mysqlserver.php @@ -0,0 +1,89 @@ + + * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 + */ + +use Froxlor\UI\Callbacks\Admin; +use Froxlor\UI\Listing; + +return [ + 'mysqlserver_list' => [ + 'title' => lng('admin.mysqlserver.mysqlserver'), + 'icon' => 'fa-solid fa-server', + 'self_overview' => ['section' => 'mysqlserver', 'page' => 'mysqlserver'], + 'columns' => [ + 'id' => [ + 'label' => lng('admin.mysqlserver.dbserver'), + 'field' => 'id', + ], + 'caption' => [ + 'label' => lng('admin.mysqlserver.caption'), + 'field' => 'caption', + ], + 'host' => [ + 'label' => lng('admin.mysqlserver.host'), + 'field' => 'host', + ], + 'port' => [ + 'label' => lng('admin.mysqlserver.port'), + 'field' => 'port', + 'class' => 'text-center', + ], + 'user' => [ + 'label' => lng('admin.mysqlserver.user'), + 'field' => 'user', + 'visible' => [Admin::class, 'canChangeServerSettings'] + ], + ], + 'visible_columns' => Listing::getVisibleColumnsForListing('mysqlserver_list', [ + 'caption', + 'host', + 'port', + ]), + 'actions' => [ + 'edit' => [ + 'icon' => 'fa fa-edit', + 'title' => lng('panel.edit'), + 'href' => [ + 'section' => 'mysqlserver', + 'page' => 'mysqlserver', + 'action' => 'edit', + 'id' => ':id' + ], + 'visible' => [Admin::class, 'canChangeServerSettings'] + ], + 'delete' => [ + 'icon' => 'fa fa-trash', + 'title' => lng('panel.delete'), + 'class' => 'btn-danger', + 'href' => [ + 'section' => 'mysqlserver', + 'page' => 'mysqlserver', + 'action' => 'delete', + 'id' => ':id' + ], + 'visible' => [Admin::class, 'canChangeServerSettings'] + ], + ] + ] +]; diff --git a/lib/tablelisting/customer/tablelisting.mysqls.php b/lib/tablelisting/customer/tablelisting.mysqls.php index 21fbe1dd..ae5b3861 100644 --- a/lib/tablelisting/customer/tablelisting.mysqls.php +++ b/lib/tablelisting/customer/tablelisting.mysqls.php @@ -50,7 +50,7 @@ return [ 'label' => lng('mysql.mysql_server'), 'field' => 'dbserver', 'callback' => [Mysql::class, 'dbserver'], - 'visible' => $count_mysqlservers > 1 + 'visible' => $multiple_mysqlservers ] ], 'visible_columns' => Listing::getVisibleColumnsForListing('mysql_list', [ diff --git a/lng/de.lng.php b/lng/de.lng.php index 7e871f2a..0a338c51 100644 --- a/lng/de.lng.php +++ b/lng/de.lng.php @@ -458,6 +458,22 @@ return [ 'smtpsettings' => 'SMTP Einstellungen', 'smtptestaddr' => 'Test-Email senden an', 'smtptestnote' => 'Bitte beachten: Die untenstehenden Werte reflektieren die aktuellen Einstellungen und können auch nur dort angepasst werden (siehe Link in der oberen rechten Ecke)', + 'mysqlserver' => [ + 'caption' => 'Beschreibung', + 'user' => 'Privilegierter Benutzer', + 'add' => 'MySQL Server hinzufügen', + 'edit' => 'MySQL Server bearbeiten', + 'password' => 'Passwort privilegierter Benutzer', + 'password_emptynochange' => 'Neues Passwort, leer für keine Änderung', + 'allowall' => [ + 'title' => 'Nutzung für aktuelle Kunden automatisch erlauben', + 'description' => 'Ist diese Einstellung aktiv, wird die Verwendung dieses Datenbank-Servers automatisch allen aktuell existierenden Kunden-Accounts erlaubt. Diese Einstellung ist nicht permanent, kann aber mehrfach / nach Bedarf ausgeführt werden.', + ], + 'testconn' => 'Teste Verbindung beim Speichern', + 'ssl' => 'Verwende SSL für die Verbindung zum Datenbank-Server', + 'ssl_cert_file' => 'Dateipfad zur SSL certificate authority', + 'verify_ca' => 'Aktiviere SSL Zertifikats-Verifikation', + ], ], 'apikeys' => [ 'no_api_keys' => 'Keine API Keys gefunden', diff --git a/lng/en.lng.php b/lng/en.lng.php index a860aebe..5fc52531 100644 --- a/lng/en.lng.php +++ b/lng/en.lng.php @@ -465,6 +465,26 @@ return [ 'smtpsettings' => 'SMTP settings', 'smtptestaddr' => 'Send test-mail to', 'smtptestnote' => 'Note that the values below reflect your current settings and can only be adjusted there (see link in top right corner)', + 'mysqlserver' => [ + 'mysqlserver' => 'MySQL Server', + 'dbserver' => 'Server #', + 'caption' => 'Description', + 'host' => 'Hostname / IP', + 'port' => 'Port', + 'user' => 'Privileged user', + 'add' => 'Add new MySQL server', + 'edit' => 'Edit MySQL server', + 'password' => 'Privileged user password', + 'password_emptynochange' => 'New password, leave empty for no change', + 'allowall' => [ + 'title' => 'Allow use of this server to all currently existing customers', + 'description' => 'Set this to "true" if you want to allow use of this database-server to all currently existing customers so they can add databases on it. This setting is not permanent but can be run multiple times.', + ], + 'testconn' => 'Test connection when saving', + 'ssl' => 'Use SSL for connection to database-server', + 'ssl_cert_file' => 'The file path to the SSL certificate authority', + 'verify_ca' => 'Enable verification of the server SSL certificate', + ], ], 'apcuinfo' => [ 'clearcache' => 'Clear APCu cache',