1560759c51
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.
806 lines
39 KiB
PHP
806 lines
39 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
return static function (AppContext $ctx): void {
|
|
$ok = [];
|
|
$errors = [];
|
|
|
|
$daUser = $ctx->daUser->username();
|
|
$prefix = $ctx->daUser->prefix();
|
|
$showRemoteHosts = $ctx->settings->allowRemoteHosts();
|
|
$fixedHosts = array_flip($ctx->mysql->requiredAccessHosts());
|
|
$dbLimit = $ctx->daUser->maxDatabases();
|
|
|
|
if ($showRemoteHosts && !$ctx->isPost()) {
|
|
$sync = $ctx->mysql->syncHostRules();
|
|
if (!$sync['ok']) {
|
|
$errors[] = 'Synchronizacja zdalnych hostów MySQL nie powiodła się: ' . $sync['output'];
|
|
}
|
|
}
|
|
|
|
$generatedPassword = '';
|
|
$generatedRole = '';
|
|
$generatedDb = '';
|
|
$advancedPosted = false;
|
|
$formAction = $ctx->postString('form_action');
|
|
$deletePromptDbRaw = '';
|
|
$deletePromptSelectedRolesRaw = [];
|
|
$deleteBackupJobIdRaw = '';
|
|
$deleteBackupJobId = '';
|
|
$deleteBackupDbName = '';
|
|
$backupQueue = new BackupQueueService($ctx->settings, $ctx->daUser);
|
|
$backupEnabled = $ctx->settings->enableBackup();
|
|
$uploadBackupEnabled = $ctx->settings->enableUploadBackup();
|
|
$pluginErrorLog = PLUGIN_ROOT . '/error.log';
|
|
$logDownload = static function (string $message) use ($pluginErrorLog, $ctx, $daUser): void {
|
|
$entry = sprintf(
|
|
"[%s] DOWNLOAD action=%s user=%s remote=%s method=%s uri=%s %s\n",
|
|
date('c'),
|
|
$ctx->action(),
|
|
$daUser,
|
|
Http::server('REMOTE_ADDR'),
|
|
Http::method(),
|
|
Http::server('REQUEST_URI'),
|
|
$message
|
|
);
|
|
@error_log($entry, 3, $pluginErrorLog);
|
|
};
|
|
|
|
$streamBackupDownload = static function (string $downloadJobId) use ($backupQueue, $logDownload): void {
|
|
$downloadJobId = trim($downloadJobId);
|
|
if ($downloadJobId === '') {
|
|
throw new RuntimeException('Brak identyfikatora zadania backupu.');
|
|
}
|
|
|
|
$logDownload('start job_id=' . $downloadJobId);
|
|
$downloadPath = $backupQueue->resolveBackupTargetFileForCurrentUser($downloadJobId);
|
|
if (!is_file($downloadPath)) {
|
|
throw new RuntimeException('Plik backupu nie istnieje na serwerze.');
|
|
}
|
|
if (!is_readable($downloadPath)) {
|
|
throw new RuntimeException('Brak uprawnień odczytu pliku backupu.');
|
|
}
|
|
|
|
$downloadName = basename($downloadPath);
|
|
$downloadName = preg_replace('/[^A-Za-z0-9._-]+/', '_', $downloadName) ?? 'backup.sql';
|
|
$contentType = 'application/octet-stream';
|
|
if (preg_match('/\.sql$/i', $downloadName)) {
|
|
$contentType = 'application/sql';
|
|
} elseif (preg_match('/\.gz$/i', $downloadName)) {
|
|
$contentType = 'application/gzip';
|
|
}
|
|
|
|
clearstatcache(true, $downloadPath);
|
|
$contentLength = @filesize($downloadPath);
|
|
$logDownload(
|
|
'prepare job_id=' . $downloadJobId .
|
|
' file=' . $downloadPath .
|
|
' size=' . (is_int($contentLength) ? (string)$contentLength : 'unknown') .
|
|
' ob_level=' . (string)ob_get_level()
|
|
);
|
|
|
|
while (ob_get_level() > 0) {
|
|
@ob_end_clean();
|
|
}
|
|
|
|
if (function_exists('apache_setenv')) {
|
|
@apache_setenv('no-gzip', '1');
|
|
}
|
|
@ini_set('zlib.output_compression', '0');
|
|
@ini_set('output_buffering', '0');
|
|
|
|
if (headers_sent($hsFile, $hsLine)) {
|
|
$logDownload('headers_sent job_id=' . $downloadJobId . ' file=' . $hsFile . ' line=' . (string)$hsLine);
|
|
throw new RuntimeException('Nie można pobrać backupu (nagłówki zostały już wysłane).');
|
|
}
|
|
|
|
header_remove('Content-Encoding');
|
|
header('Content-Type: ' . $contentType);
|
|
header('Content-Disposition: attachment; filename="' . str_replace('"', '', $downloadName) . '"');
|
|
header('X-Content-Type-Options: nosniff');
|
|
header('Cache-Control: no-store, no-cache, must-revalidate');
|
|
header('Pragma: no-cache');
|
|
if (is_int($contentLength) && $contentLength > 0) {
|
|
header('Content-Length: ' . (string)$contentLength);
|
|
}
|
|
|
|
$bytes = @readfile($downloadPath);
|
|
if ($bytes === false) {
|
|
throw new RuntimeException('Nie można odczytać pliku backupu do pobrania.');
|
|
}
|
|
$logDownload('done job_id=' . $downloadJobId . ' bytes=' . (string)$bytes);
|
|
exit;
|
|
};
|
|
|
|
$action = Http::getString($_REQUEST, 'action');
|
|
if ($action === 'download_backup') {
|
|
try {
|
|
$ctx->requireCsrf('download_backup_file_submit');
|
|
$streamBackupDownload(Http::getString($_REQUEST, 'job'));
|
|
} catch (Throwable $e) {
|
|
$logDownload('error job_id=' . trim(Http::getString($_REQUEST, 'job')) . ' message=' . $e->getMessage());
|
|
http_response_code(400);
|
|
header('Content-Type: text/plain; charset=UTF-8');
|
|
echo 'Nie można pobrać backupu: ' . $e->getMessage();
|
|
exit;
|
|
}
|
|
}
|
|
|
|
if ($ctx->isPost() && $formAction === 'queue_backup') {
|
|
try {
|
|
$ctx->requireCsrf('queue_backup_submit');
|
|
if (!$backupEnabled) {
|
|
throw new RuntimeException('Tworzenie backupu jest wyłączone w konfiguracji pluginu.');
|
|
}
|
|
|
|
$dbName = NamePolicy::normalizeDatabaseName($ctx->postString('backup_db'), $prefix);
|
|
$availableDbs = array_map(static fn(array $db): string => $db['name'], $ctx->mysql->listDatabasesForUser($daUser));
|
|
if (!in_array($dbName, $availableDbs, true)) {
|
|
throw new RuntimeException('Wskazana baza danych nie należy do konta: ' . $dbName);
|
|
}
|
|
|
|
$backupQueue->queueBackupJob($dbName);
|
|
$ok[] = 'Kopia zapasowa bazy danych ' . $dbName . ' została dodana do kolejki';
|
|
} catch (Throwable $e) {
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
if ($ctx->isPost() && $formAction === 'delete_backup_file') {
|
|
try {
|
|
$ctx->requireCsrf('delete_backup_file_submit');
|
|
$jobId = trim($ctx->postString('backup_job_id'));
|
|
if ($jobId === '') {
|
|
throw new RuntimeException('Brak identyfikatora zadania backupu.');
|
|
}
|
|
|
|
$deletedPath = $backupQueue->deleteBackupTargetFileForCurrentUser($jobId);
|
|
try {
|
|
$backupQueue->deleteBackupLogForCurrentUser($jobId);
|
|
} catch (Throwable $logErr) {
|
|
// Ignore log cleanup failure.
|
|
}
|
|
$ok[] = 'Usunięto plik backupu: ' . $deletedPath;
|
|
} catch (Throwable $e) {
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
if ($ctx->isPost() && $formAction === 'delete_backup_confirm') {
|
|
try {
|
|
$ctx->requireCsrf('delete_backup_confirm_submit');
|
|
$jobId = trim($ctx->postString('backup_job_id'));
|
|
if ($jobId === '') {
|
|
throw new RuntimeException('Brak identyfikatora zadania backupu.');
|
|
}
|
|
|
|
$deletedPath = $backupQueue->deleteBackupTargetFileForCurrentUser($jobId);
|
|
try {
|
|
$backupQueue->deleteBackupLogForCurrentUser($jobId);
|
|
} catch (Throwable $logErr) {
|
|
// Ignore log cleanup failure.
|
|
}
|
|
$ok[] = 'Usunięto plik backupu: ' . $deletedPath;
|
|
$deleteBackupJobIdRaw = '';
|
|
} catch (Throwable $e) {
|
|
$errors[] = $e->getMessage();
|
|
$deleteBackupJobIdRaw = trim($ctx->postString('backup_job_id'));
|
|
}
|
|
}
|
|
|
|
if ($ctx->isPost() && $formAction === 'download_backup_file') {
|
|
try {
|
|
$ctx->requireCsrf('download_backup_file_submit');
|
|
$streamBackupDownload($ctx->postString('backup_job_id'));
|
|
} catch (Throwable $e) {
|
|
$logDownload('error job_id=' . trim($ctx->postString('backup_job_id')) . ' message=' . $e->getMessage());
|
|
$errors[] = 'Nie można pobrać backupu: ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
if ($ctx->isPost() && $formAction === 'create_database') {
|
|
try {
|
|
$ctx->requireCsrf('create_database_submit');
|
|
|
|
$limit = $ctx->daUser->maxDatabases();
|
|
$used = $ctx->mysql->countDatabasesForUser($daUser);
|
|
if ($limit > 0 && $used >= $limit) {
|
|
throw new RuntimeException('Osiągnięto limit baz danych dla konta (' . $used . '/' . $limit . ').');
|
|
}
|
|
|
|
$dbRaw = trim($ctx->postString('db_suffix'));
|
|
if ($dbRaw === '') {
|
|
$dbRaw = trim($ctx->postString('db_name'));
|
|
}
|
|
$dbName = NamePolicy::normalizeDatabaseName($dbRaw, $prefix);
|
|
if ($ctx->mysql->databaseExists($dbName)) {
|
|
throw new RuntimeException('Baza danych już istnieje: ' . $dbName);
|
|
}
|
|
|
|
$creationMode = strtolower($ctx->postString('creation_mode', 'simple'));
|
|
$roleModeRaw = strtolower($ctx->postString('role_mode', 'new'));
|
|
$advancedPosted = ($creationMode === 'advanced');
|
|
if (!$advancedPosted) {
|
|
$advancedPosted =
|
|
trim($ctx->postString('role_name')) !== '' ||
|
|
trim($ctx->postString('password')) !== '' ||
|
|
trim($ctx->postString('existing_role')) !== '' ||
|
|
$roleModeRaw === 'existing';
|
|
}
|
|
|
|
$roleMode = ($advancedPosted && $roleModeRaw === 'existing') ? 'existing' : 'new';
|
|
$selectedRole = '';
|
|
$createdRole = false;
|
|
$createdDatabase = false;
|
|
|
|
if ($roleMode === 'existing') {
|
|
$selectedRole = NamePolicy::normalizeRoleName($ctx->postString('existing_role'), $prefix);
|
|
if (!$ctx->mysql->roleExists($selectedRole)) {
|
|
throw new RuntimeException('Wybrany użytkownik MySQL nie istnieje: ' . $selectedRole);
|
|
}
|
|
} else {
|
|
$roleInput = $ctx->postString('role_name');
|
|
if ($roleInput === '') {
|
|
$roleInput = $dbName;
|
|
}
|
|
$selectedRole = NamePolicy::normalizeRoleName($roleInput, $prefix);
|
|
if ($ctx->mysql->roleExists($selectedRole)) {
|
|
throw new RuntimeException('Użytkownik MySQL już istnieje: ' . $selectedRole);
|
|
}
|
|
|
|
$password = $ctx->postString('password');
|
|
if ($advancedPosted && $password === '') {
|
|
throw new RuntimeException('Hasło użytkownika jest wymagane w trybie zaawansowanym.');
|
|
}
|
|
if (!$advancedPosted) {
|
|
$password = rtrim(strtr(base64_encode(random_bytes(18)), '+/', 'AZ'), '=');
|
|
}
|
|
if (strlen($password) < 12) {
|
|
throw new RuntimeException('Hasło użytkownika musi mieć co najmniej 12 znaków.');
|
|
}
|
|
|
|
$ctx->mysql->createRole($selectedRole, $password);
|
|
$createdRole = true;
|
|
$generatedPassword = $password;
|
|
}
|
|
|
|
try {
|
|
$ctx->mysql->createDatabase($dbName, $selectedRole);
|
|
$createdDatabase = true;
|
|
|
|
$privileges = NamePolicy::sanitizePrivileges($ctx->postArray('privileges'));
|
|
if (empty($privileges)) {
|
|
$privileges = NamePolicy::defaultPrivileges();
|
|
}
|
|
$ctx->mysql->grantPrivileges($dbName, $selectedRole, $privileges);
|
|
|
|
$hostEntries = $ctx->mysql->getRoleHostEntries($daUser, $selectedRole);
|
|
$hostMap = [];
|
|
foreach ($hostEntries as $entry) {
|
|
$hostMap[$entry['host']] = $entry['note'];
|
|
}
|
|
|
|
$needsHostUpdate = $createdRole;
|
|
|
|
if ($showRemoteHosts && $advancedPosted) {
|
|
$remoteHostRaw = trim($ctx->postString('remote_host'));
|
|
if ($remoteHostRaw !== '') {
|
|
$remoteHost = NamePolicy::normalizeRemoteIpv4Host($remoteHostRaw);
|
|
$remoteComment = NamePolicy::normalizeHostComment($ctx->postString('remote_comment'));
|
|
if (!array_key_exists($remoteHost, $hostMap) || $hostMap[$remoteHost] !== $remoteComment) {
|
|
$needsHostUpdate = true;
|
|
}
|
|
$hostMap[$remoteHost] = $remoteComment;
|
|
}
|
|
}
|
|
|
|
if ($needsHostUpdate) {
|
|
$customHostsCount = 0;
|
|
foreach (array_keys($hostMap) as $host) {
|
|
if (!isset($fixedHosts[$host])) {
|
|
$customHostsCount++;
|
|
}
|
|
}
|
|
if ($customHostsCount > $ctx->settings->maxHostsPerUser()) {
|
|
throw new RuntimeException('Przekroczono limit hostów dla użytkownika MySQL.');
|
|
}
|
|
|
|
$finalEntries = [];
|
|
foreach ($hostMap as $host => $note) {
|
|
$finalEntries[] = ['host' => $host, 'note' => $note];
|
|
}
|
|
$ctx->mysql->replaceRoleHostsWithNotes($daUser, $selectedRole, $finalEntries);
|
|
}
|
|
|
|
$sync = $ctx->mysql->syncHostRules();
|
|
if (!$sync['ok']) {
|
|
$errors[] = 'Baza i uprawnienia zapisane, ale synchronizacja hostów MySQL nie powiodła się: ' . $sync['output'];
|
|
}
|
|
|
|
$generatedRole = $selectedRole;
|
|
$generatedDb = $dbName;
|
|
$ok[] = 'Baza danych została utworzona.';
|
|
|
|
// Reset the create-database form so the next database can be
|
|
// created without manually clearing the previous input.
|
|
$_POST['db_suffix'] = '';
|
|
$_POST['db_name'] = '';
|
|
$_POST['role_name'] = '';
|
|
$_POST['password'] = '';
|
|
$_POST['existing_role'] = '';
|
|
$_POST['role_mode'] = 'new';
|
|
$_POST['creation_mode'] = 'simple';
|
|
$_POST['remote_host'] = '';
|
|
$_POST['remote_comment'] = '';
|
|
$advancedPosted = false;
|
|
} catch (Throwable $inner) {
|
|
if ($createdDatabase) {
|
|
try {
|
|
$ctx->mysql->dropDatabase($dbName);
|
|
} catch (Throwable $ignored) {
|
|
}
|
|
}
|
|
if ($createdRole) {
|
|
try {
|
|
$ctx->mysql->dropRole($selectedRole);
|
|
} catch (Throwable $ignored) {
|
|
}
|
|
}
|
|
throw $inner;
|
|
}
|
|
} catch (Throwable $e) {
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
if ($ctx->isPost() && $formAction === 'delete_database_confirm') {
|
|
try {
|
|
$ctx->requireCsrf('delete_database_submit');
|
|
|
|
$deletePromptDbRaw = trim($ctx->postString('delete_db'));
|
|
if ($deletePromptDbRaw === '') {
|
|
throw new RuntimeException('Nie wskazano bazy danych do usunięcia.');
|
|
}
|
|
|
|
$deletePromptSelectedRolesRaw = $ctx->postArray('delete_roles');
|
|
$deleteMode = strtolower(trim($ctx->postString('delete_mode')));
|
|
$dbToDelete = NamePolicy::normalizeDatabaseName($deletePromptDbRaw, $prefix);
|
|
|
|
$availableBeforeDelete = array_map(static fn(array $db): string => $db['name'], $ctx->mysql->listDatabasesForUser($daUser));
|
|
if (!in_array($dbToDelete, $availableBeforeDelete, true)) {
|
|
throw new RuntimeException('Wskazana baza danych nie należy do konta: ' . $dbToDelete);
|
|
}
|
|
|
|
$assignedRoles = array_values(array_unique($ctx->mysql->listDatabaseUsers($daUser, $dbToDelete)));
|
|
$singleMatchingRole = count($assignedRoles) === 1 && $assignedRoles[0] === $dbToDelete;
|
|
foreach ($assignedRoles as $roleName) {
|
|
$ctx->mysql->revokePrivileges($dbToDelete, $roleName);
|
|
}
|
|
|
|
$ctx->mysql->dropDatabase($dbToDelete);
|
|
|
|
$rolesToDelete = [];
|
|
if ($deleteMode === 'with_user' && $singleMatchingRole) {
|
|
$rolesToDelete[] = $assignedRoles[0];
|
|
} elseif ($deleteMode !== 'db_only') {
|
|
foreach ($deletePromptSelectedRolesRaw as $rawRole) {
|
|
$roleName = NamePolicy::normalizeRoleName($rawRole, $prefix);
|
|
if (!in_array($roleName, $assignedRoles, true)) {
|
|
continue;
|
|
}
|
|
if (!in_array($roleName, $rolesToDelete, true)) {
|
|
$rolesToDelete[] = $roleName;
|
|
}
|
|
}
|
|
}
|
|
|
|
$deletedUsers = [];
|
|
$skippedUsers = [];
|
|
foreach ($rolesToDelete as $roleName) {
|
|
$remainingAssigned = array_values(array_unique($ctx->mysql->roleAssignedDatabases($roleName, $daUser)));
|
|
foreach ($remainingAssigned as $remainingDb) {
|
|
$ctx->mysql->revokePrivileges($remainingDb, $roleName);
|
|
}
|
|
|
|
$remainingOwned = array_values(array_unique($ctx->mysql->roleOwnedDatabases($roleName, $daUser)));
|
|
if (!empty($remainingOwned)) {
|
|
$skippedUsers[] = $roleName . ' (jest właścicielem baz: ' . implode(', ', $remainingOwned) . ')';
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$ctx->mysql->deleteRoleHosts($daUser, $roleName);
|
|
$ctx->mysql->dropRole($roleName);
|
|
$deletedUsers[] = $roleName;
|
|
} catch (Throwable $dropErr) {
|
|
$skippedUsers[] = $roleName . ' (' . $dropErr->getMessage() . ')';
|
|
}
|
|
}
|
|
|
|
$sync = $ctx->mysql->syncHostRules();
|
|
if (!$sync['ok']) {
|
|
$errors[] = 'Usuwanie wykonane, ale synchronizacja hostów MySQL nie powiodła się: ' . $sync['output'];
|
|
}
|
|
|
|
if (count($deletedUsers) === 1) {
|
|
$ok[] = 'Usunięto bazę danych ' . $dbToDelete . ' i użytkownika ' . $deletedUsers[0] . '.';
|
|
} elseif (count($deletedUsers) > 1) {
|
|
$listItems = '';
|
|
foreach ($deletedUsers as $deletedUser) {
|
|
$listItems .= "\n• " . $deletedUser;
|
|
}
|
|
$ok[] = 'Usunięto bazę danych ' . $dbToDelete . ' i następujących użytkowników przypisanych do bazy danych ' . $dbToDelete . ':' . $listItems;
|
|
} else {
|
|
$ok[] = 'Usunięto bazę danych ' . $dbToDelete . '.';
|
|
}
|
|
if (!empty($skippedUsers)) {
|
|
$errors[] = 'Nie udało się usunąć części użytkowników: ' . implode('; ', $skippedUsers) . '.';
|
|
}
|
|
|
|
$deletePromptDbRaw = '';
|
|
$deletePromptSelectedRolesRaw = [];
|
|
} catch (Throwable $e) {
|
|
$errors[] = $e->getMessage();
|
|
if ($deletePromptDbRaw === '') {
|
|
$deletePromptDbRaw = trim($ctx->postString('delete_db'));
|
|
}
|
|
if (empty($deletePromptSelectedRolesRaw)) {
|
|
$deletePromptSelectedRolesRaw = $ctx->postArray('delete_roles');
|
|
}
|
|
}
|
|
}
|
|
|
|
$showTableCount = $ctx->settings->enableTableCount();
|
|
$showDbSize = $ctx->settings->countDatabaseSize();
|
|
$databases = $ctx->mysql->listDatabasesForUser($daUser, $showDbSize);
|
|
$dbUsersMap = $ctx->mysql->listDatabaseUsersMap($daUser);
|
|
$roles = $ctx->mysql->listRolesForUser($daUser);
|
|
$roleNames = array_map(static fn(array $r): string => $r['name'], $roles);
|
|
$dbCount = count($databases);
|
|
$endpoint = $ctx->mysql->connectionEndpoint();
|
|
$endpointHost = trim($endpoint['host']) !== '' ? $endpoint['host'] : 'localhost';
|
|
$endpointPort = $endpoint['port'] > 0 ? (string)$endpoint['port'] : '3306';
|
|
$serverVersion = '';
|
|
try {
|
|
$serverVersion = $ctx->mysql->serverVersion();
|
|
} catch (Throwable $e) {
|
|
$serverVersion = '';
|
|
}
|
|
$advancedOpen = $advancedPosted || (strtolower($ctx->postString('creation_mode', 'simple')) === 'advanced');
|
|
if (!$ctx->isPost() && $deletePromptDbRaw === '') {
|
|
$deletePromptDbRaw = trim($ctx->queryString('delete_db'));
|
|
}
|
|
if (!$ctx->isPost() && $deleteBackupJobIdRaw === '') {
|
|
$deleteBackupJobIdRaw = trim($ctx->queryString('delete_backup'));
|
|
}
|
|
/** @var array<string,string> $overlayHtmlById */
|
|
$overlayHtmlById = [];
|
|
$deletePromptDb = '';
|
|
$deletePromptRows = '';
|
|
$deletePromptWarnings = [];
|
|
$deletePromptSelectedRoles = [];
|
|
$deletePromptSingleRoleMode = false;
|
|
$deletePromptSingleRoleName = '';
|
|
|
|
$limitText = $dbLimit === 0 ? 'Bez ograniczeń' : (string)$dbLimit;
|
|
$usageWarn = '';
|
|
|
|
$tableCounts = [];
|
|
if ($showTableCount) {
|
|
foreach ($databases as $dbRow) {
|
|
$dbName = $dbRow['name'];
|
|
try {
|
|
$stats = $ctx->mysql->getDatabaseObjectStats($dbName);
|
|
$tableCounts[$dbName] = (int)$stats['tables'];
|
|
} catch (Throwable $e) {
|
|
$tableCounts[$dbName] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
$dbRows = '';
|
|
foreach ($databases as $db) {
|
|
$name = $db['name'];
|
|
$size = $db['size'];
|
|
$users = $dbUsersMap[$name] ?? [];
|
|
|
|
$dbRows .= '<tr>';
|
|
$dbRows .= '<td><strong>' . AppContext::e($name) . '</strong></td>';
|
|
if ($showDbSize) {
|
|
$dbRows .= '<td>' . AppContext::e($size) . '</td>';
|
|
}
|
|
$dbRows .= '<td>' . AppContext::e((string)count($users)) . '</td>';
|
|
if ($showTableCount) {
|
|
$dbRows .= '<td>' . AppContext::e((string)($tableCounts[$name] ?? 0)) . '</td>';
|
|
}
|
|
$dbRows .= '<td class="actions">';
|
|
if ($ctx->settings->enableAdminer()) {
|
|
$dbRows .= '<form method="post" target="_top" action="' . AppContext::e($ctx->url('open_phpmyadmin.raw')) . '" style="display:inline-flex;gap:6px;margin:0">';
|
|
$dbRows .= $ctx->csrfField('open_phpmyadmin_submit');
|
|
$dbRows .= '<input type="hidden" name="adminer_db" value="' . AppContext::e($name) . '">';
|
|
$dbRows .= '<button class="btn adminer-login" type="submit">Zaloguj do bazy</button>';
|
|
$dbRows .= '</form>';
|
|
}
|
|
if ($backupEnabled) {
|
|
$locked = $backupQueue->isDbLocked($name);
|
|
if (!$locked) {
|
|
$dbRows .= '<form method="post" style="display:inline-flex;gap:6px;margin:0">';
|
|
$dbRows .= $ctx->csrfField('queue_backup_submit');
|
|
$dbRows .= '<input type="hidden" name="form_action" value="queue_backup">';
|
|
$dbRows .= '<input type="hidden" name="backup_db" value="' . AppContext::e($name) . '">';
|
|
$dbRows .= '<button class="btn" type="submit">Wykonaj Backup</button>';
|
|
$dbRows .= '</form>';
|
|
} else {
|
|
$dbRows .= '<button class="btn" type="button" disabled title="Na tej bazie trwa już operacja backup/restore">Wykonaj Backup</button>';
|
|
}
|
|
}
|
|
$dbRows .= '<a class="btn" href="' . AppContext::e($ctx->url('modify_privileges.html', ['db' => $name])) . '">Zarządzaj</a>';
|
|
$dbRows .= '<a class="btn danger-fill" href="' . AppContext::e($ctx->url('index.html', ['delete_db' => $name])) . '">Usuń bazę</a>';
|
|
$dbRows .= '</td>';
|
|
$dbRows .= '</tr>';
|
|
}
|
|
if ($dbRows === '') {
|
|
$colspan = 3 + ($showDbSize ? 1 : 0) + ($showTableCount ? 1 : 0);
|
|
$dbRows = '<tr><td colspan="' . AppContext::e((string)$colspan) . '" class="muted">Nie znaleziono baz danych...</td></tr>';
|
|
}
|
|
|
|
if ($deletePromptDbRaw !== '') {
|
|
try {
|
|
$deletePromptDb = NamePolicy::normalizeDatabaseName($deletePromptDbRaw, $prefix);
|
|
$dbNames = array_map(static fn(array $db): string => $db['name'], $databases);
|
|
if (!in_array($deletePromptDb, $dbNames, true)) {
|
|
throw new RuntimeException('Wskazana baza danych nie należy do konta: ' . $deletePromptDb);
|
|
}
|
|
|
|
foreach ($deletePromptSelectedRolesRaw as $rawRole) {
|
|
$roleName = NamePolicy::normalizeRoleName($rawRole, $prefix);
|
|
if (!in_array($roleName, $deletePromptSelectedRoles, true)) {
|
|
$deletePromptSelectedRoles[] = $roleName;
|
|
}
|
|
}
|
|
|
|
$assignedRoles = array_values(array_unique($ctx->mysql->listDatabaseUsers($daUser, $deletePromptDb)));
|
|
if (count($assignedRoles) === 1 && $assignedRoles[0] === $deletePromptDb) {
|
|
$deletePromptSingleRoleMode = true;
|
|
$deletePromptSingleRoleName = $assignedRoles[0];
|
|
} else {
|
|
foreach ($assignedRoles as $roleName) {
|
|
$assignedDbs = array_values(array_unique($ctx->mysql->roleAssignedDatabases($roleName, $daUser)));
|
|
$isChecked = in_array($roleName, $deletePromptSelectedRoles, true) ? ' checked' : '';
|
|
$deletePromptRows .= '<tr>';
|
|
$deletePromptRows .= '<td><input type="checkbox" class="delete-role-checkbox" name="delete_roles[]" value="' . AppContext::e($roleName) . '"' . $isChecked . '></td>';
|
|
$deletePromptRows .= '<td>' . AppContext::e($roleName) . '</td>';
|
|
|
|
if (empty($assignedDbs)) {
|
|
$deletePromptRows .= '<td class="muted">Brak przypisań</td>';
|
|
} else {
|
|
$dbList = '';
|
|
foreach ($assignedDbs as $dbName) {
|
|
$dbList .= '<li>' . AppContext::e($dbName) . '</li>';
|
|
}
|
|
$deletePromptRows .= '<td><ul class="list-clean">' . $dbList . '</ul></td>';
|
|
}
|
|
$deletePromptRows .= '</tr>';
|
|
|
|
if (count($assignedDbs) > 1) {
|
|
$deletePromptWarnings[] = 'Uwaga: wskazany użytkownik jest przypisany do wielu baz danych i jego usunięcie spowoduje brak możliwości dostępu do innych baz danych przy użyciu danych logowania użytkownika ' . $roleName . '.';
|
|
}
|
|
}
|
|
}
|
|
} catch (Throwable $e) {
|
|
$errors[] = $e->getMessage();
|
|
$deletePromptDb = '';
|
|
$deletePromptRows = '';
|
|
$deletePromptWarnings = [];
|
|
$deletePromptSelectedRoles = [];
|
|
$deletePromptSingleRoleMode = false;
|
|
$deletePromptSingleRoleName = '';
|
|
}
|
|
}
|
|
|
|
$roleOptions = '<option value="">-- wybierz istniejącego użytkownika --</option>';
|
|
$postedExistingRole = $ctx->postString('existing_role');
|
|
foreach ($roleNames as $roleName) {
|
|
$selected = ($postedExistingRole === $roleName) ? ' selected' : '';
|
|
$roleOptions .= '<option value="' . AppContext::e($roleName) . '"' . $selected . '>' . AppContext::e($roleName) . '</option>';
|
|
}
|
|
|
|
$privCheckboxes = '';
|
|
foreach (NamePolicy::PRIVILEGE_LABELS as $code => $label) {
|
|
$checked = ($code === 'ALL') ? ' checked' : '';
|
|
$privCheckboxes .= '<label class="inline"><input type="checkbox" name="privileges[]" value="' . AppContext::e($code) . '"' . $checked . '> ' . AppContext::e($label) . '</label>';
|
|
}
|
|
|
|
$successCard = '';
|
|
if ($generatedDb !== '' && $generatedRole !== '') {
|
|
$passwordLine = $generatedPassword !== ''
|
|
? '<div><strong>Hasło:</strong></div><div><code>' . AppContext::e($generatedPassword) . '</code></div>'
|
|
: '';
|
|
|
|
$successCard .= '<section class="success-credentials">';
|
|
$successCard .= '<div class="left">✓</div>';
|
|
$successCard .= '<div class="right">';
|
|
$successCard .= '<h4>Dane dostępowe do bazy danych</h4>';
|
|
$successCard .= '<div class="kv">';
|
|
$successCard .= '<div><strong>Nazwa hosta:</strong></div><div><code>' . AppContext::e($endpointHost) . ' (Port: ' . AppContext::e($endpointPort) . ')</code></div>';
|
|
$successCard .= '<div><strong>Baza danych:</strong></div><div><code>' . AppContext::e($generatedDb) . '</code></div>';
|
|
$successCard .= '<div><strong>Nazwa użytkownika:</strong></div><div><code>' . AppContext::e($generatedRole) . '</code></div>';
|
|
$successCard .= $passwordLine;
|
|
$successCard .= '</div></div></section>';
|
|
}
|
|
|
|
$body = '';
|
|
$body .= $successCard;
|
|
$body .= $usageWarn;
|
|
$phpMyAdminWarning = $ctx->settings->phpMyAdminIntegrationWarning();
|
|
if ($phpMyAdminWarning !== '') {
|
|
$errors[] = $phpMyAdminWarning;
|
|
}
|
|
|
|
if ($deleteBackupJobIdRaw !== '') {
|
|
try {
|
|
$deleteBackupJobId = trim($deleteBackupJobIdRaw);
|
|
$job = $backupQueue->getJobForCurrentUser($deleteBackupJobId);
|
|
if (($job['type'] ?? '') !== 'backup') {
|
|
throw new RuntimeException('Wskazane zadanie nie jest zadaniem backupu.');
|
|
}
|
|
if (($job['status'] ?? '') !== 'done_ok') {
|
|
throw new RuntimeException('Backup nie został zakończony powodzeniem.');
|
|
}
|
|
$deleteBackupDbName = (string)($job['db_name'] ?? '');
|
|
if ($deleteBackupDbName === '') {
|
|
$deleteBackupDbName = 'nieznana baza';
|
|
}
|
|
|
|
$body .= '<section class="panel" style="margin-bottom:14px;border-color:#f0b4a7;background:#fff5f3">';
|
|
$body .= '<div class="alert alert-err" style="margin:0 0 10px">Czy na pewno chcesz usunąć backup bazy danych - <strong>' . AppContext::e($deleteBackupDbName) . '</strong>?</div>';
|
|
$body .= '<form method="post">';
|
|
$body .= $ctx->csrfField('delete_backup_confirm_submit');
|
|
$body .= '<input type="hidden" name="form_action" value="delete_backup_confirm">';
|
|
$body .= '<input type="hidden" name="backup_job_id" value="' . AppContext::e($deleteBackupJobId) . '">';
|
|
$body .= '<div class="actions">';
|
|
$body .= '<button class="danger" type="submit">Potwierdź usunięcie backupu</button>';
|
|
$body .= '<a class="btn" href="' . AppContext::e($ctx->url('index.html')) . '">Anuluj</a>';
|
|
$body .= '</div>';
|
|
$body .= '</form>';
|
|
$body .= '</section>';
|
|
} catch (Throwable $e) {
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
if ($deletePromptDb !== '') {
|
|
$body .= '<section class="panel" style="margin-bottom:14px;border-color:#f0b4a7;background:#fff5f3">';
|
|
if ($deletePromptSingleRoleMode && $deletePromptSingleRoleName !== '') {
|
|
$body .= '<div class="alert alert-err" style="margin:0 0 10px">Czy na pewno chcesz usunąć następującą bazę danych - <strong>' . AppContext::e($deletePromptDb) . '</strong> i przypisanego do niej użytkownika <strong>' . AppContext::e($deletePromptSingleRoleName) . '</strong>.</div>';
|
|
} else {
|
|
$body .= '<div class="alert alert-err" style="margin:0 0 10px">Czy na pewno chcesz usunąć następującą bazę danych - <strong>' . AppContext::e($deletePromptDb) . '</strong>.</div>';
|
|
}
|
|
$body .= '<form method="post">';
|
|
$body .= $ctx->csrfField('delete_database_submit');
|
|
$body .= '<input type="hidden" name="form_action" value="delete_database_confirm">';
|
|
$body .= '<input type="hidden" name="delete_db" value="' . AppContext::e($deletePromptDb) . '">';
|
|
|
|
if ($deletePromptSingleRoleMode && $deletePromptSingleRoleName !== '') {
|
|
$body .= '<div class="actions">';
|
|
$body .= '<button class="danger-fill" type="submit" name="delete_mode" value="with_user">Tak</button>';
|
|
$body .= '<a class="btn" href="' . AppContext::e($ctx->url('index.html')) . '">Nie</a>';
|
|
$body .= '<button class="btn warn" type="submit" name="delete_mode" value="db_only">Usuń tylko bazę danych</button>';
|
|
$body .= '</div>';
|
|
} else {
|
|
if ($deletePromptRows !== '') {
|
|
$body .= '<p class="section-text">Do bazy danych ' . AppContext::e($deletePromptDb) . ' przypisani są użytkownicy, z poniższej listy wybierz użytkowników, których chcesz usunąć.</p>';
|
|
$body .= '<table><thead><tr>';
|
|
$body .= '<th data-select-all-for="delete-role-checkbox" title="Kliknij, aby zaznaczyć lub odznaczyć wszystkich użytkowników">Wybierz</th>';
|
|
$body .= '<th>Nazwa użytkownika</th>';
|
|
$body .= '<th>Przypisane Bazy danych</th>';
|
|
$body .= '</tr></thead><tbody>' . $deletePromptRows . '</tbody></table>';
|
|
} else {
|
|
$body .= '<p class="muted">Do wskazanej bazy nie są przypisani użytkownicy MySQL.</p>';
|
|
}
|
|
|
|
if (!empty($deletePromptWarnings)) {
|
|
foreach (array_values(array_unique($deletePromptWarnings)) as $warning) {
|
|
$body .= '<div class="alert alert-err">' . AppContext::e($warning) . '</div>';
|
|
}
|
|
}
|
|
|
|
$body .= '<div class="actions">';
|
|
$body .= '<button class="danger" type="submit">Potwierdź usunięcie bazy</button>';
|
|
$body .= '<a class="btn" href="' . AppContext::e($ctx->url('index.html')) . '">Anuluj</a>';
|
|
$body .= '</div>';
|
|
}
|
|
$body .= '</form>';
|
|
$body .= '</section>';
|
|
}
|
|
|
|
$dbSuffixValue = $ctx->postString('db_suffix');
|
|
if (strpos($dbSuffixValue, $prefix) === 0) {
|
|
$dbSuffixValue = substr($dbSuffixValue, strlen($prefix));
|
|
}
|
|
$roleSuffixValue = $ctx->postString('role_name');
|
|
if (strpos($roleSuffixValue, $prefix) === 0) {
|
|
$roleSuffixValue = substr($roleSuffixValue, strlen($prefix));
|
|
}
|
|
|
|
$body .= '<section class="panel">';
|
|
$body .= '<h3 class="section-title-center">Lista baz danych</h3>';
|
|
$body .= '<div class="actions" style="justify-content:space-between;align-items:center;margin-bottom:8px">';
|
|
if ($serverVersion !== '') {
|
|
$body .= '<span class="muted">' . AppContext::e($ctx->t('Wersja serwera MySQL')) . ': <strong>' . AppContext::e($serverVersion) . '</strong></span>';
|
|
}
|
|
$body .= '<span class="muted">Ilość baz danych <strong>' . AppContext::e((string)$dbCount) . '/' . AppContext::e($limitText) . '</strong></span>';
|
|
$body .= '</div>';
|
|
$header = '<th>Nazwa bazy danych</th>';
|
|
if ($showDbSize) {
|
|
$header .= '<th>Rozmiar</th>';
|
|
}
|
|
$header .= '<th>Użytkownicy</th>';
|
|
if ($showTableCount) {
|
|
$header .= '<th>Liczba Tabel</th>';
|
|
}
|
|
$header .= '<th>Akcje</th>';
|
|
$body .= '<table><thead><tr>' . $header . '</tr></thead><tbody>' . $dbRows . '</tbody></table>';
|
|
$body .= '</section>';
|
|
|
|
$body .= '<section class="panel" style="margin-top:14px">';
|
|
$body .= '<h3>Utwórz bazę danych</h3>';
|
|
$body .= '<p class="section-text">Tryb prosty tworzy użytkownika o tej samej nazwie co baza i automatycznie generuje hasło.</p>';
|
|
$body .= '<form method="post">';
|
|
$body .= $ctx->csrfField('create_database_submit');
|
|
$body .= '<input type="hidden" name="form_action" value="create_database">';
|
|
$body .= '<input type="hidden" id="create-db-mode" name="creation_mode" value="' . ($advancedOpen ? 'advanced' : 'simple') . '">';
|
|
$body .= '<input type="hidden" name="role_mode" value="new">';
|
|
$body .= '<div><label>Nazwa bazy danych</label>';
|
|
$body .= '<div class="input-prefix"><span class="prefix">' . AppContext::e($prefix) . '</span><input type="text" name="db_suffix" value="' . AppContext::e($dbSuffixValue) . '" required></div>';
|
|
$body .= '</div>';
|
|
$body .= '<p class="muted" style="margin-top:6px">Użytkownik o tej samej nazwie i bezpiecznym haśle zostanie wygenerowany automatycznie.</p>';
|
|
|
|
$body .= '<details class="details-advanced" id="create-db-advanced"' . ($advancedOpen ? ' open' : '') . '>';
|
|
$body .= '<summary>Tryb zaawansowany</summary>';
|
|
$body .= '<div class="row" style="margin-top:10px">';
|
|
$body .= '<div>';
|
|
$roleModeCurrent = strtolower($ctx->postString('role_mode', 'new'));
|
|
$body .= '<label class="inline"><input type="radio" id="role-mode-new" name="role_mode" value="new"' . ($roleModeCurrent !== 'existing' ? ' checked' : '') . '> Nowy użytkownik</label>';
|
|
$body .= '<label class="inline"><input type="radio" id="role-mode-existing" name="role_mode" value="existing"' . ($roleModeCurrent === 'existing' ? ' checked' : '') . '> Istniejący użytkownik</label>';
|
|
$body .= '</div>';
|
|
$body .= '<div><label>Istniejący użytkownik</label><select id="existing-role-name" name="existing_role">' . $roleOptions . '</select></div>';
|
|
$body .= '</div>';
|
|
|
|
$body .= '<div class="row">';
|
|
$body .= '<div><label>Nazwa użytkownika MySQL</label><div class="input-prefix"><span class="prefix">' . AppContext::e($prefix) . '</span><input type="text" id="advanced-role-name" name="role_name" value="' . AppContext::e($roleSuffixValue) . '" autocomplete="off"></div></div>';
|
|
$body .= '<div><label>Hasło użytkownika</label>';
|
|
$body .= '<div class="input-with-tools"><input type="password" id="advanced-role-password" name="password" value="' . AppContext::e($ctx->postString('password')) . '" autocomplete="new-password">';
|
|
$body .= '<span class="field-tools">';
|
|
$body .= '<button type="button" class="tool-btn" data-generate-target="advanced-role-password" title="Generuj hasło" aria-label="Generuj hasło"><svg viewBox="0 0 24 24"><path d="M20 7v5h-5"></path><path d="M20 12a8 8 0 1 1-2.34-5.66L20 7"></path></svg></button>';
|
|
$body .= '<button type="button" class="tool-btn" data-toggle-target="advanced-role-password" title="Pokaż lub ukryj hasło" aria-label="Pokaż lub ukryj hasło"><svg viewBox="0 0 24 24"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"></path><circle cx="12" cy="12" r="3"></circle></svg></button>';
|
|
$body .= '</span></div>';
|
|
$body .= '<p class="muted" style="margin-top:6px">W trybie zaawansowanym hasło jest wymagane.</p></div>';
|
|
$body .= '</div>';
|
|
|
|
if ($showRemoteHosts) {
|
|
$defaultHostHelp = '`localhost` jest dodawany automatycznie.';
|
|
if (count($fixedHosts) > 1) {
|
|
$defaultHostHelp .= ' Przy zdalnym serwerze MySQL plugin dodaje tez adres IP serwera DirectAdmin.';
|
|
}
|
|
$body .= '<div class="row">';
|
|
$body .= '<div><label>Dodatkowy host IPv4 (opcjonalnie)</label><input type="text" name="remote_host" placeholder="203.0.113.10"></div>';
|
|
$body .= '<div><label>Komentarz hosta (opcjonalnie)</label><input type="text" name="remote_comment" placeholder="np. serwer aplikacji"></div>';
|
|
$body .= '</div>';
|
|
$body .= '<p class="muted" style="margin-top:6px">' . $defaultHostHelp . '</p>';
|
|
}
|
|
|
|
$body .= '<div><label>Uprawnienia początkowe</label><div class="privileges-group" data-privileges-group>' . $privCheckboxes . '</div></div>';
|
|
$body .= '</details>';
|
|
|
|
$body .= '<div class="actions" style="justify-content:flex-end"><button class="primary" type="submit">UTWÓRZ</button></div>';
|
|
$body .= '</form>';
|
|
$body .= '</section>';
|
|
|
|
if (!empty($overlayHtmlById)) {
|
|
$body .= implode('', array_values($overlayHtmlById));
|
|
}
|
|
|
|
$ctx->renderPage('Zarządzanie bazami danych MySQL', $body, $ok, $errors);
|
|
};
|