Files
alt-mysql/exec/lib/Settings.php
T
Marek Miklewicz 1560759c51 Scope phpMyAdmin SSO to one database, fix logout, reset create form
After live testing on the real server, three follow-up issues surfaced:

- PhpMyAdminSso granted the temporary SSO MySQL role access to every
  database the account owns, not just the one the "Zaloguj do bazy"
  button was clicked for, so phpMyAdmin showed all databases. Extract
  determineGrantTargets() and scope the GRANT to only the requested
  database; also set phpMyAdmin's only_db from the SSO session so the
  UI itself only shows that database.

- phpMyAdmin's LogoutURL pointed at da_login.php with no ticket, which
  always produced "Missing or invalid phpMyAdmin login ticket." on
  logout. Add a DIRECTADMIN_PANEL_PORT setting (default 2222) and
  point LogoutURL at the plugin's own database list
  (/CMD_PLUGINS/alt-mysql/index.html) instead.

- The create-database form kept echoing back the just-submitted
  values after a successful creation, forcing manual clearing before
  creating the next database. Clear the relevant $_POST keys once
  creation succeeds so the form resets while still preserving sticky
  values on validation failure.

Bump version to 1.2.14 and rebuild the release archive.
2026-07-04 20:38:04 +02:00

479 lines
15 KiB
PHP

<?php
declare(strict_types=1);
final class Settings
{
/** @var array<string,string> */
private array $values;
private function __construct(array $values)
{
$this->values = $values;
}
public static function load(string $path): self
{
$values = [];
if (is_readable($path)) {
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines !== false) {
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (!preg_match('/^([A-Za-z0-9_]+)=(.*)$/', $line, $m)) {
continue;
}
$values[$m[1]] = self::parseShValue((string)$m[2]);
}
}
}
return new self($values);
}
/**
* @return array{host:string,port:int,database:string,user:string,password:string,socket:string}
*/
public static function loadMysqlCredentials(string $path): array
{
if (!is_readable($path)) {
throw new RuntimeException('Brak pliku z danymi MySQL: ' . $path);
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
throw new RuntimeException('Nie można odczytać pliku: ' . $path);
}
$kv = [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (strpos($line, '=') !== false) {
[$k, $v] = array_map('trim', explode('=', $line, 2));
if ($k !== '') {
$kv[strtoupper($k)] = self::parseShValue($v);
}
continue;
}
$parts = explode(':', $line);
if (count($parts) >= 5) {
return [
'host' => trim($parts[0]) !== '' ? trim($parts[0]) : 'localhost',
'port' => (int)(trim($parts[1]) !== '' ? trim($parts[1]) : '3306'),
'database' => trim($parts[2]) !== '' ? trim($parts[2]) : 'mysql',
'user' => trim($parts[3]) !== '' ? trim($parts[3]) : 'root',
'password' => trim(implode(':', array_slice($parts, 4))),
'socket' => '',
];
}
}
$host = trim((string)($kv['HOST'] ?? $kv['MYSQL_HOST'] ?? ''));
if ($host === '') {
$host = 'localhost';
}
$portRaw = trim((string)($kv['PORT'] ?? $kv['MYSQL_PORT'] ?? '3306'));
$port = preg_match('/^[0-9]+$/', $portRaw) ? (int)$portRaw : 3306;
if ($port < 1 || $port > 65535) {
$port = 3306;
}
$database = trim((string)($kv['DATABASE'] ?? $kv['MYSQL_DATABASE'] ?? $kv['DB'] ?? 'mysql'));
if ($database === '' || $database === '*') {
$database = 'mysql';
}
$user = trim((string)($kv['USER'] ?? $kv['MYSQL_USER'] ?? 'root'));
$password = trim((string)($kv['PASSWORD'] ?? $kv['PASSWD'] ?? $kv['MYSQL_PASSWORD'] ?? ''));
$socket = trim((string)($kv['SOCKET'] ?? $kv['MYSQL_SOCKET'] ?? ''));
if ($user === '') {
throw new RuntimeException('Brak użytkownika MySQL w pliku: ' . $path);
}
return [
'host' => $host,
'port' => $port,
'database' => $database,
'user' => $user,
'password' => $password,
'socket' => $socket,
];
}
public function mysqlCredentialsFile(): string
{
$path = trim($this->getString('MYSQL_PLUGIN_CREDENTIALS_FILE', '/usr/local/directadmin/conf/alt-mysql.conf'));
return $path !== '' ? $path : '/usr/local/directadmin/conf/alt-mysql.conf';
}
public function mysqlClientBinDir(): string
{
return rtrim(trim($this->getString('MYSQL_PLUGIN_CLIENT_BIN_DIR', '')), '/');
}
public function getString(string $key, string $default = ''): string
{
return isset($this->values[$key]) ? trim($this->values[$key]) : $default;
}
public function getInt(string $key, int $default = 0): int
{
if (!isset($this->values[$key])) {
return $default;
}
$value = trim($this->values[$key]);
if ($value === '' || !preg_match('/^-?[0-9]+$/', $value)) {
return $default;
}
return (int)$value;
}
public function getBool(string $key, bool $default = false): bool
{
if (!isset($this->values[$key])) {
return $default;
}
return self::toBool($this->values[$key], $default);
}
public function defaultDatabasesLimit(): int
{
$limit = $this->getInt('MYSQL_PLUGIN_DEFAULT_DB_LIMIT', 5);
return $limit < 0 ? 0 : $limit;
}
public function alwaysEnabled(): bool
{
return $this->getBool('always_enabled', true);
}
public function maxHostsPerUser(): int
{
$limit = $this->getInt('MYSQL_PLUGIN_MAX_HOSTS_PER_USER', 30);
return $limit < 1 ? 1 : $limit;
}
public function allowRemoteHosts(): bool
{
if (array_key_exists('allow_remote_hosts', $this->values)) {
return self::toBool($this->values['allow_remote_hosts'], false);
}
if (array_key_exists('MYSQL_PLUGIN_ALLOW_REMOTE_HOSTS', $this->values)) {
return self::toBool($this->values['MYSQL_PLUGIN_ALLOW_REMOTE_HOSTS'], false);
}
return false;
}
public function modifyServerConfiguration(): bool
{
if ($this->allowRemoteHosts()) {
return true;
}
if (array_key_exists('modify_server_configuration', $this->values)) {
return self::toBool($this->values['modify_server_configuration'], false);
}
if (array_key_exists('MYSQL_PLUGIN_MODIFY_SERVER_CONFIGURATION', $this->values)) {
return self::toBool($this->values['MYSQL_PLUGIN_MODIFY_SERVER_CONFIGURATION'], false);
}
return false;
}
public function hostSyncScript(): string
{
$default = '/usr/local/directadmin/plugins/alt-mysql/scripts/setup/mysql_host_sync.sh';
$path = $this->getString('MYSQL_PLUGIN_SUDO_SYNC_HOSTS', $default);
return $path !== '' ? $path : $default;
}
public function enableBackup(): bool
{
return $this->getBool('ENABLE_BACKUP', true);
}
public function enableUploadBackup(): bool
{
return $this->getBool('ENABLE_UPLOAD_BACKUP', true);
}
public function enableAdminer(): bool
{
if (!$this->getBool('ENABLE_PHPMYADMIN', $this->getBool('ENABLE_ADMINER', true))) {
return false;
}
return $this->isPhpMyAdminInstalled();
}
public function phpMyAdminInstallDir(): string
{
$path = trim($this->getString('PHPMYADMIN_INSTALL_DIR', '/var/www/html/alt-mysql-phpmyadmin'));
return $path !== '' ? rtrim($path, '/') : '/var/www/html/alt-mysql-phpmyadmin';
}
public function adminerUrlPath(): string
{
$path = trim($this->getString('PHPMYADMIN_URL_PATH', $this->getString('ADMINER_URL_PATH', '/alt-mysql-phpmyadmin')));
if ($path === '') {
return '/alt-mysql-phpmyadmin';
}
if ($path[0] !== '/') {
$path = '/' . $path;
}
$path = rtrim($path, '/');
return $path !== '' ? $path : '/alt-mysql-phpmyadmin';
}
public function adminerPublicBaseUrl(): string
{
$url = trim($this->getString('PHPMYADMIN_PUBLIC_BASE_URL', $this->getString('ADMINER_PUBLIC_BASE_URL', '')));
if ($url === '') {
return '';
}
$url = rtrim($url, '/');
if (!preg_match('#^https?://#i', $url)) {
return '';
}
return $url;
}
public function adminerPublic(): bool
{
return $this->getBool('PHPMYADMIN_PUBLIC', $this->getBool('ADMINER_PUBLIC', false));
}
public function adminerSsoDir(): string
{
$path = trim($this->getString('PHPMYADMIN_SSO_DIR', $this->getString('ADMINER_SSO_DIR', $this->phpMyAdminInstallDir() . '/runtime')));
if ($path === '') {
$path = $this->phpMyAdminInstallDir() . '/runtime';
}
return rtrim($path, '/');
}
public function adminerTicketTtl(): int
{
$ttl = $this->getInt('PHPMYADMIN_TICKET_TTL', $this->getInt('ADMINER_TICKET_TTL', 120));
if ($ttl < 10) {
return 10;
}
if ($ttl > 300) {
return 300;
}
return $ttl;
}
public function adminerSessionTtl(): int
{
$ttl = $this->getInt('PHPMYADMIN_SESSION_TTL', $this->getInt('ADMINER_SESSION_TTL', 900));
if ($ttl < 60) {
return 60;
}
if ($ttl > 3600) {
return 3600;
}
return $ttl;
}
public function adminerRoleTtl(): int
{
$ttl = $this->getInt('PHPMYADMIN_ROLE_TTL', $this->getInt('ADMINER_ROLE_TTL', 1200));
if ($ttl < 120) {
return 120;
}
if ($ttl > 7200) {
return 7200;
}
return $ttl;
}
public function directAdminPanelPort(): string
{
$port = trim($this->getString('DIRECTADMIN_PANEL_PORT', '2222'));
return ctype_digit($port) && $port !== '' ? $port : '2222';
}
public function phpMyAdminIntegrationWarning(): string
{
if (!$this->getBool('ENABLE_PHPMYADMIN', $this->getBool('ENABLE_ADMINER', true))) {
return '';
}
$dir = $this->phpMyAdminInstallDir();
if (!is_dir($dir)) {
return 'Prywatna kopia phpMyAdmin dla pluginu nie jest zainstalowana. Uruchom scripts/setup/phpmyadmin_private_install.sh jako root.';
}
$config = $dir . '/config.inc.php';
$signon = $dir . '/da_signon.php';
$signonUrl = $dir . '/da_login.php';
if (!is_readable($config)) {
return 'Prywatny phpMyAdmin pluginu jest zainstalowany, ale nie można odczytać config.inc.php.';
}
if (!is_readable($signon)) {
return 'Prywatny phpMyAdmin pluginu jest zainstalowany, ale brakuje da_signon.php. Uruchom scripts/setup/phpmyadmin_repair.sh jako root.';
}
if (!is_readable($signonUrl)) {
return 'Prywatny phpMyAdmin pluginu jest zainstalowany, ale brakuje da_login.php. Uruchom scripts/setup/phpmyadmin_repair.sh jako root.';
}
$contents = file_get_contents($config);
if (!is_string($contents) || strpos($contents, 'DA_MYSQL_PMA_DEFAULT_CONF') === false) {
return 'Prywatny phpMyAdmin pluginu nie ma konfiguracji alt-mysql. Uruchom scripts/setup/phpmyadmin_repair.sh jako root.';
}
if (strpos($contents, "SignonURL'] = da_mysql_phpmyadmin_signon_url") === false) {
return 'Prywatny phpMyAdmin pluginu nie ma wymaganej konfiguracji SignonURL. Uruchom scripts/setup/phpmyadmin_repair.sh jako root.';
}
$signonUrlContents = file_get_contents($signonUrl);
if (!is_string($signonUrlContents) || strpos($signonUrlContents, 'da_mysql_login_consume_ticket') === false) {
return 'Prywatny phpMyAdmin pluginu nie obsługuje jednorazowych ticketów SSO. Uruchom scripts/setup/phpmyadmin_repair.sh jako root.';
}
return '';
}
public function allowRestoreToOtherDatabase(): bool
{
return $this->getBool('ALLOW_RESTORE_TO_OTHER_DATABASE', true);
}
public function enableTableCount(): bool
{
return $this->getBool('ENABLE_TABLE_COUNT', true);
}
public function countDatabaseSize(): bool
{
return $this->getBool('COUNT_DATABASE_SIZE', true);
}
public function compressBackup(): bool
{
return $this->getBool('COMPRESS_BACKUP', true);
}
public function hitmeBackupLocation(string $daUser = ''): string
{
$path = $this->getString('HITME_BACKUP_LOCATION', '/home/admin/mysql_backup');
if ($path === '') {
$path = '/home/admin/mysql_backup';
}
if ($daUser !== '') {
$path = str_replace(['DA_USER', 'da_user'], $daUser, $path);
}
return rtrim($path, '/');
}
public function userBaseBackupDir(string $daUser = ''): string
{
$path = $this->getString('USER_BASE_BACKUP_DIR', '/home/DA_USER/mysql_backup');
if ($path === '') {
$path = '/home/DA_USER/mysql_backup';
}
if ($daUser !== '') {
$path = str_replace(['DA_USER', 'da_user'], $daUser, $path);
}
return rtrim($path, '/');
}
public function userBackupDateFormat(): string
{
$format = $this->getString('USER_BACKUP_DATE_FORMAT', 'd.m.Y-H.i');
return $format !== '' ? $format : 'd.m.Y-H.i';
}
public function loadOrCreateSecret(string $path): string
{
if (is_readable($path)) {
$value = trim((string)file_get_contents($path));
if ($value !== '') {
return $value;
}
}
$secret = bin2hex(random_bytes(32));
$dir = dirname($path);
if (!is_dir($dir)) {
@mkdir($dir, 0700, true);
}
if (@file_put_contents($path, $secret) !== false) {
@chmod($path, 0600);
return $secret;
}
return hash('sha256', __FILE__ . '|' . php_uname('n') . '|' . $path . '|alt-mysql');
}
public static function toBool(string $value, bool $default = false): bool
{
$v = strtolower(trim($value));
if ($v === '') {
return $default;
}
if (in_array($v, ['1', 'true', 'yes', 'y', 'on', 'checked'], true)) {
return true;
}
if (in_array($v, ['0', 'false', 'no', 'n', 'off', 'unchecked'], true)) {
return false;
}
return $default;
}
private function isPhpMyAdminInstalled(): bool
{
$dir = $this->phpMyAdminInstallDir();
return is_dir($dir) && is_readable($dir . '/index.php') && is_readable($dir . '/config.inc.php');
}
private static function parseShValue(string $value): string
{
$value = trim($value);
if ($value === '') {
return '';
}
if ($value[0] === '\'' && substr($value, -1) === '\'') {
return substr($value, 1, -1);
}
if ($value[0] === '"' && substr($value, -1) === '"') {
return stripcslashes(substr($value, 1, -1));
}
$value = preg_replace('/\s+#.*$/', '', $value);
return trim((string)$value);
}
}