793 lines
38 KiB
PHP
793 lines
38 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.';
|
|
} 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);
|
|
};
|