Import alt-mysql plugin

This commit is contained in:
Marek Miklewicz
2026-07-04 15:48:19 +02:00
commit d71fcf9ace
101 changed files with 18635 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$ok = [];
$errors = [];
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$dbNames = array_map(static fn(array $db): string => $db['name'], $ctx->mysql->listDatabasesForUser($daUser));
$roleNames = array_map(static fn(array $r): string => $r['name'], $ctx->mysql->listRolesForUser($daUser));
if ($ctx->isPost()) {
try {
$ctx->requireCsrf('assign_database_submit');
$dbName = NamePolicy::normalizeDatabaseName($ctx->postString('db_name'), $prefix);
$roleName = NamePolicy::normalizeRoleName($ctx->postString('role_name'), $prefix);
if (!in_array($dbName, $dbNames, true)) {
throw new RuntimeException('Baza nie należy do bieżącego użytkownika DA: ' . $dbName);
}
if (!in_array($roleName, $roleNames, true)) {
throw new RuntimeException('Użytkownik MySQL nie należy do bieżącego konta DA: ' . $roleName);
}
$privileges = NamePolicy::sanitizePrivileges($ctx->postArray('privileges'));
if (empty($privileges)) {
$privileges = NamePolicy::defaultPrivileges();
}
$ctx->mysql->grantPrivileges($dbName, $roleName, $privileges);
$hostEntries = $ctx->mysql->getRoleHostEntries($daUser, $roleName);
$hasLocalhost = false;
foreach ($hostEntries as $entry) {
if ($entry['host'] === 'localhost') {
$hasLocalhost = true;
break;
}
}
if (!$hasLocalhost) {
$hostEntries[] = ['host' => 'localhost', 'note' => ''];
$ctx->mysql->replaceRoleHostsWithNotes($daUser, $roleName, $hostEntries);
}
$sync = $ctx->mysql->syncHostRules();
if (!$sync['ok']) {
$errors[] = 'Przypisanie wykonane, ale synchronizacja hostów MySQL nie powiodła się: ' . $sync['output'];
}
$ok[] = 'Przypisano bazę ' . $dbName . ' do użytkownika ' . $roleName . '.';
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
$dbOptions = '<option value="">-- wybierz bazę --</option>';
foreach ($dbNames as $dbName) {
$dbOptions .= '<option value="' . AppContext::e($dbName) . '">' . AppContext::e($dbName) . '</option>';
}
$roleOptions = '<option value="">-- wybierz użytkownika --</option>';
foreach ($roleNames as $roleName) {
$roleOptions .= '<option value="' . AppContext::e($roleName) . '">' . 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>';
}
$body = '';
$body .= '<form method="post">';
$body .= $ctx->csrfField('assign_database_submit');
$body .= '<div class="row">';
$body .= '<div><label>Baza danych</label><select name="db_name" required>' . $dbOptions . '</select></div>';
$body .= '<div><label>Użytkownik MySQL</label><select name="role_name" required>' . $roleOptions . '</select></div>';
$body .= '</div>';
$body .= '<div class="panel"><h3>Uprawnienia</h3><div class="privileges-group" data-privileges-group>' . $privCheckboxes . '</div></div>';
$body .= '<div class="actions"><button class="primary" type="submit">Przypisz bazę</button></div>';
$body .= '</form>';
$ctx->renderPage('Przypisywanie Bazy do Użytkownika', $body, $ok, $errors);
};
+563
View File
@@ -0,0 +1,563 @@
<?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());
$passwordChangedFor = '';
$changedPassword = '';
$createdRoleName = '';
$createdRolePassword = '';
$createdRoleAssignedDbs = [];
$showCreateUserForm = (strtolower(trim($ctx->queryString('add_user'))) === '1');
$deletePromptRoleRaw = trim($ctx->postString('delete_role'));
if ($deletePromptRoleRaw === '') {
$deletePromptRoleRaw = trim($ctx->queryString('delete_role'));
}
$deletePromptRole = '';
$deletePromptAssignedDbs = [];
$createRoleSuffixInput = trim($ctx->postString('create_role_name'));
if (strpos($createRoleSuffixInput, $prefix) === 0) {
$createRoleSuffixInput = substr($createRoleSuffixInput, strlen($prefix));
}
$createPasswordInput = $ctx->postString('create_password');
$createSelectedDbsRaw = $ctx->postArray('create_user_databases');
$createSelectedDbs = array_values(array_unique(array_filter($createSelectedDbsRaw, static fn(string $db): bool => $db !== '')));
$databases = $ctx->mysql->listDatabasesForUser($daUser);
$allDbs = array_map(static fn(array $db): string => $db['name'], $databases);
$roles = $ctx->mysql->listRolesForUser($daUser);
$roleNames = array_map(static fn(array $r): string => $r['name'], $roles);
$preferredRoleRaw = $ctx->postString('role_name');
if ($preferredRoleRaw === '') {
$preferredRoleRaw = $ctx->queryString('role');
}
if ($ctx->isPost()) {
$postAction = $ctx->postString('form_action');
try {
if ($postAction === 'create_user_inline') {
$ctx->requireCsrf('create_user_inline_submit');
$showCreateUserForm = true;
$newRole = NamePolicy::normalizeRoleName($ctx->postString('create_role_name'), $prefix);
if ($ctx->mysql->roleExists($newRole)) {
throw new RuntimeException('Użytkownik MySQL już istnieje: ' . $newRole);
}
$newPassword = $ctx->postString('create_password');
if (strlen($newPassword) < 12) {
throw new RuntimeException('Hasło użytkownika musi mieć co najmniej 12 znaków.');
}
$selectedDbs = [];
foreach ($createSelectedDbs as $rawDbName) {
$dbName = NamePolicy::normalizeDatabaseName($rawDbName, $prefix);
if (!in_array($dbName, $allDbs, true)) {
throw new RuntimeException('Baza nie należy do konta DA: ' . $dbName);
}
if (!in_array($dbName, $selectedDbs, true)) {
$selectedDbs[] = $dbName;
}
}
$ctx->mysql->createRole($newRole, $newPassword);
try {
$ctx->mysql->replaceRoleHostsWithNotes($daUser, $newRole, []);
foreach ($selectedDbs as $dbName) {
$ctx->mysql->grantPrivileges($dbName, $newRole, NamePolicy::defaultPrivileges());
}
$sync = $ctx->mysql->syncHostRules();
if (!$sync['ok']) {
$errors[] = 'Użytkownik został utworzony, ale synchronizacja hostów MySQL nie powiodła się: ' . $sync['output'];
}
$createdRoleName = $newRole;
$createdRolePassword = $newPassword;
$createdRoleAssignedDbs = $selectedDbs;
if (empty($selectedDbs)) {
$ok[] = 'Utworzono użytkownika ' . $newRole . ' bez przypisanych baz danych.';
} else {
$ok[] = 'Utworzono użytkownika ' . $newRole . ' i przypisano do ' . count($selectedDbs) . ' baz(y).';
}
$preferredRoleRaw = $newRole;
$showCreateUserForm = false;
$createRoleSuffixInput = '';
$createPasswordInput = '';
$createSelectedDbs = [];
} catch (Throwable $inner) {
try {
$ctx->mysql->deleteRoleHosts($daUser, $newRole);
} catch (Throwable $ignored) {
}
try {
$ctx->mysql->dropRole($newRole);
} catch (Throwable $ignored) {
}
throw $inner;
}
} elseif ($postAction === 'delete_user_confirm') {
$ctx->requireCsrf('delete_user_submit');
$roleToDelete = NamePolicy::normalizeRoleName($ctx->postString('delete_role'), $prefix);
if (!in_array($roleToDelete, $roleNames, true)) {
throw new RuntimeException('Użytkownik MySQL nie należy do konta DA: ' . $roleToDelete);
}
$owned = array_values(array_unique($ctx->mysql->roleOwnedDatabases($roleToDelete, $daUser)));
if (!empty($owned)) {
throw new RuntimeException(
'Nie można usunąć użytkownika, ponieważ jest właścicielem baz: ' . implode(', ', $owned)
);
}
$assigned = array_values(array_unique($ctx->mysql->roleAssignedDatabases($roleToDelete, $daUser)));
foreach ($assigned as $dbName) {
$owner = $ctx->mysql->getDatabaseOwner($dbName);
if ($owner !== null && $owner !== $roleToDelete) {
$ctx->mysql->reassignOwnedObjects($dbName, $roleToDelete, $owner);
}
$ctx->mysql->revokePrivileges($dbName, $roleToDelete);
}
$ctx->mysql->deleteRoleHosts($daUser, $roleToDelete);
$ctx->mysql->dropRole($roleToDelete);
$sync = $ctx->mysql->syncHostRules();
if (!$sync['ok']) {
$errors[] = 'Usuwanie wykonane, ale synchronizacja hostów MySQL nie powiodła się: ' . $sync['output'];
}
$ok[] = 'Usunięto użytkownika ' . $roleToDelete . '.';
if ($preferredRoleRaw === $roleToDelete) {
$preferredRoleRaw = '';
}
$deletePromptRoleRaw = '';
} else {
$ctx->requireCsrf('user_manage_submit');
if (empty($roleNames)) {
throw new RuntimeException('Brak użytkowników MySQL do zarządzania.');
}
$selectedRole = NamePolicy::normalizeRoleName($ctx->postString('role_name', $preferredRoleRaw), $prefix);
if (!in_array($selectedRole, $roleNames, true)) {
throw new RuntimeException('Użytkownik MySQL nie należy do konta DA: ' . $selectedRole);
}
$preferredRoleRaw = $selectedRole;
if ($postAction === 'change_password') {
$password = $ctx->postString('password');
if (strlen($password) < 12) {
throw new RuntimeException('Nowe hasło musi mieć minimum 12 znaków.');
}
$ctx->mysql->changeRolePassword($selectedRole, $password);
$passwordChangedFor = $selectedRole;
$changedPassword = $password;
} elseif ($postAction === 'grant_db') {
$dbName = NamePolicy::normalizeDatabaseName($ctx->postString('db_name'), $prefix);
if (!in_array($dbName, $allDbs, true)) {
throw new RuntimeException('Baza nie należy do konta DA: ' . $dbName);
}
$privileges = NamePolicy::sanitizePrivileges($ctx->postArray('privileges'));
if (empty($privileges)) {
$privileges = NamePolicy::defaultPrivileges();
}
$ctx->mysql->grantPrivileges($dbName, $selectedRole, $privileges);
$ok[] = 'Przyznano dostęp do bazy ' . $dbName . ' użytkownikowi ' . $selectedRole . '.';
} elseif ($postAction === 'revoke_db') {
$dbName = NamePolicy::normalizeDatabaseName($ctx->postString('db_name'), $prefix);
$ctx->mysql->revokePrivileges($dbName, $selectedRole);
$ok[] = 'Odebrano dostęp do bazy ' . $dbName . ' użytkownikowi ' . $selectedRole . '.';
} elseif ($postAction === 'add_host' && $showRemoteHosts) {
$host = NamePolicy::normalizeRemoteIpv4Host($ctx->postString('host'));
$note = NamePolicy::normalizeHostComment($ctx->postString('host_note'));
$entries = $ctx->mysql->getRoleHostEntries($daUser, $selectedRole);
$map = [];
foreach ($entries as $entry) {
$map[$entry['host']] = $entry['note'];
}
$map[$host] = $note;
$customHostsCount = 0;
foreach (array_keys($map) as $mapHost) {
if (!isset($fixedHosts[$mapHost])) {
$customHostsCount++;
}
}
if ($customHostsCount > $ctx->settings->maxHostsPerUser()) {
throw new RuntimeException('Przekroczono limit hostów dla użytkownika MySQL.');
}
$save = [];
foreach ($map as $h => $n) {
$save[] = ['host' => $h, 'note' => $n];
}
$ctx->mysql->replaceRoleHostsWithNotes($daUser, $selectedRole, $save);
$sync = $ctx->mysql->syncHostRules();
if (!$sync['ok']) {
$errors[] = 'Host dodany, ale synchronizacja hostów MySQL nie powiodła się: ' . $sync['output'];
}
$ok[] = 'Dodano host ' . $host . ' dla użytkownika ' . $selectedRole . '.';
} elseif ($postAction === 'remove_host' && $showRemoteHosts) {
$host = trim($ctx->postString('host'));
if (isset($fixedHosts[$host])) {
throw new RuntimeException('Ten host jest dodawany automatycznie i nie moze zostac usuniety.');
}
$entries = $ctx->mysql->getRoleHostEntries($daUser, $selectedRole);
$save = [];
foreach ($entries as $entry) {
if ($entry['host'] === $host) {
continue;
}
$save[] = $entry;
}
$ctx->mysql->replaceRoleHostsWithNotes($daUser, $selectedRole, $save);
$sync = $ctx->mysql->syncHostRules();
if (!$sync['ok']) {
$errors[] = 'Host usunięty, ale synchronizacja hostów MySQL nie powiodła się: ' . $sync['output'];
}
$ok[] = 'Usunięto host ' . $host . ' dla użytkownika ' . $selectedRole . '.';
}
}
} catch (Throwable $e) {
$errors[] = $e->getMessage();
if ($postAction === 'create_user_inline') {
$showCreateUserForm = true;
}
if ($postAction === 'delete_user_confirm') {
$deletePromptRoleRaw = trim($ctx->postString('delete_role'));
}
}
}
$databases = $ctx->mysql->listDatabasesForUser($daUser);
$allDbs = array_map(static fn(array $db): string => $db['name'], $databases);
$roles = $ctx->mysql->listRolesForUser($daUser);
$roleNames = array_map(static fn(array $r): string => $r['name'], $roles);
$selectedRole = '';
if (!empty($roleNames)) {
$candidateRaw = $preferredRoleRaw !== '' ? $preferredRoleRaw : $roleNames[0];
try {
$candidate = NamePolicy::normalizeRoleName($candidateRaw, $prefix);
} catch (Throwable $e) {
$candidate = $roleNames[0];
}
if (!in_array($candidate, $roleNames, true)) {
$candidate = $roleNames[0];
}
$selectedRole = $candidate;
}
if ($deletePromptRoleRaw !== '') {
try {
$deletePromptRole = NamePolicy::normalizeRoleName($deletePromptRoleRaw, $prefix);
if (!in_array($deletePromptRole, $roleNames, true)) {
throw new RuntimeException('Użytkownik MySQL nie należy do konta DA: ' . $deletePromptRole);
}
$deletePromptAssignedDbs = array_values(array_unique($ctx->mysql->roleAssignedDatabases($deletePromptRole, $daUser)));
} catch (Throwable $e) {
$errors[] = $e->getMessage();
$deletePromptRole = '';
$deletePromptAssignedDbs = [];
}
}
$createSelectedDbSet = [];
foreach ($createSelectedDbs as $rawDbName) {
try {
$dbName = NamePolicy::normalizeDatabaseName($rawDbName, $prefix);
} catch (Throwable $e) {
continue;
}
if (in_array($dbName, $allDbs, true)) {
$createSelectedDbSet[$dbName] = true;
}
}
$roleOptions = '';
foreach ($roleNames as $roleName) {
$sel = ($roleName === $selectedRole) ? ' selected' : '';
$roleOptions .= '<option value="' . AppContext::e($roleName) . '"' . $sel . '>' . AppContext::e($roleName) . '</option>';
}
$assignedDbs = [];
$grantableDbs = $allDbs;
if ($selectedRole !== '') {
$assignedDbs = $ctx->mysql->roleAssignedDatabases($selectedRole, $daUser);
$grantableDbs = array_values(array_diff($allDbs, $assignedDbs));
}
$dbOptions = '<option value="">-- wybierz bazę danych --</option>';
foreach ($grantableDbs as $dbName) {
$dbOptions .= '<option value="' . AppContext::e($dbName) . '">' . AppContext::e($dbName) . '</option>';
}
$dbRows = '';
foreach ($assignedDbs as $dbName) {
$profile = $ctx->mysql->getGrantProfile($dbName, $selectedRole);
$label = (empty($profile) || in_array('ALL', $profile, true))
? 'Pełny dostęp'
: implode(', ', $profile);
$dbRows .= '<tr>';
$dbRows .= '<td>' . AppContext::e($dbName) . '</td>';
$dbRows .= '<td><span class="badge">' . AppContext::e($label) . '</span></td>';
$dbRows .= '<td class="actions">';
$dbRows .= '<a class="btn" href="' . AppContext::e($ctx->url('modify_privileges.html', ['db' => $dbName, 'role' => $selectedRole])) . '">Zarządzaj</a>';
$dbRows .= '<a class="btn" href="' . AppContext::e($ctx->url('modify_privileges.html', ['db' => $dbName, 'role' => $selectedRole])) . '#przywileje">Przywileje</a>';
$dbRows .= '<form method="post" style="display:inline-flex;gap:6px;margin:0">';
$dbRows .= $ctx->csrfField('user_manage_submit');
$dbRows .= '<input type="hidden" name="role_name" value="' . AppContext::e($selectedRole) . '">';
$dbRows .= '<input type="hidden" name="db_name" value="' . AppContext::e($dbName) . '">';
$dbRows .= '<input type="hidden" name="form_action" value="revoke_db">';
$dbRows .= '<button class="btn danger" type="submit">Odbierz dostęp</button>';
$dbRows .= '</form>';
$dbRows .= '</td>';
$dbRows .= '</tr>';
}
if ($dbRows === '') {
$dbRows = '<tr><td colspan="3" class="muted">Brak przypisanych baz danych.</td></tr>';
}
$hostEntries = [];
if ($selectedRole !== '') {
$hostEntries = $ctx->mysql->getRoleHostEntries($daUser, $selectedRole);
}
$hostsRows = '';
$hostsCount = count($hostEntries);
if ($showRemoteHosts && $selectedRole !== '') {
$hostsCount = count($hostEntries);
foreach ($hostEntries as $entry) {
$host = $entry['host'];
$note = $entry['note'];
$hostsRows .= '<tr>';
$hostsRows .= '<td>' . AppContext::e($host) . '</td>';
$hostsRows .= '<td>' . AppContext::e($note) . '</td>';
$hostsRows .= '<td class="actions">';
if (!isset($fixedHosts[$host])) {
$hostsRows .= '<form method="post" style="display:inline-flex;gap:6px;margin:0">';
$hostsRows .= $ctx->csrfField('user_manage_submit');
$hostsRows .= '<input type="hidden" name="role_name" value="' . AppContext::e($selectedRole) . '">';
$hostsRows .= '<input type="hidden" name="host" value="' . AppContext::e($host) . '">';
$hostsRows .= '<input type="hidden" name="form_action" value="remove_host">';
$hostsRows .= '<button class="btn danger" type="submit">Usuń</button>';
$hostsRows .= '</form>';
} else {
$hostsRows .= '<span class="muted">host stały</span>';
}
$hostsRows .= '</td>';
$hostsRows .= '</tr>';
}
if ($hostsRows === '') {
$hostsRows = '<tr><td colspan="3" class="muted">Brak dozwolonych hostów.</td></tr>';
}
}
$endpoint = $ctx->mysql->connectionEndpoint();
$endpointHost = trim($endpoint['host']) !== '' ? $endpoint['host'] : 'localhost';
$endpointPort = $endpoint['port'] > 0 ? (string)$endpoint['port'] : '3306';
$createdUserCard = '';
if ($createdRoleName !== '' && $createdRolePassword !== '') {
$assignedDbsHtml = '';
if (!empty($createdRoleAssignedDbs)) {
$assignedItems = '';
foreach ($createdRoleAssignedDbs as $assignedDbName) {
$assignedItems .= '<li><code>' . AppContext::e($assignedDbName) . '</code></li>';
}
$assignedDbsHtml .= '<div><strong>Przypisane bazy danych:</strong></div>';
$assignedDbsHtml .= '<div><ul class="list-clean">' . $assignedItems . '</ul></div>';
}
$createdUserCard .= '<section class="success-credentials">';
$createdUserCard .= '<div class="left">✓</div>';
$createdUserCard .= '<div class="right">';
$createdUserCard .= '<h4>Dane dostępowe użytkownika MySQL</h4>';
$createdUserCard .= '<div class="kv">';
$createdUserCard .= '<div><strong>Nazwa hosta:</strong></div><div><code>' . AppContext::e($endpointHost) . ' (Port: ' . AppContext::e($endpointPort) . ')</code></div>';
$createdUserCard .= '<div><strong>Nazwa użytkownika:</strong></div><div><code>' . AppContext::e($createdRoleName) . '</code></div>';
$createdUserCard .= '<div><strong>Hasło:</strong></div><div><code>' . AppContext::e($createdRolePassword) . '</code></div>';
$createdUserCard .= $assignedDbsHtml;
$createdUserCard .= '</div></div></section>';
}
$passwordChangedCard = '';
if ($passwordChangedFor !== '' && $changedPassword !== '') {
$passwordChangedCard .= '<section class="success-credentials">';
$passwordChangedCard .= '<div class="left">✓</div>';
$passwordChangedCard .= '<div class="right">';
$passwordChangedCard .= '<h4>Hasło zostało zmienione</h4>';
$passwordChangedCard .= '<div class="kv">';
$passwordChangedCard .= '<div><strong>Nazwa hosta:</strong></div><div><code>' . AppContext::e($endpointHost) . ' (Port: ' . AppContext::e($endpointPort) . ')</code></div>';
$passwordChangedCard .= '<div><strong>Nazwa użytkownika:</strong></div><div><code>' . AppContext::e($passwordChangedFor) . '</code></div>';
$passwordChangedCard .= '<div><strong>Hasło:</strong></div><div><code>' . AppContext::e($changedPassword) . '</code></div>';
$passwordChangedCard .= '</div></div></section>';
}
$body = '';
$body .= $createdUserCard;
$body .= $passwordChangedCard;
$body .= '<section class="panel">';
$body .= '<h3>Zarządzaj użytkownikami</h3>';
$body .= '<div class="actions" style="justify-content:space-between;align-items:center">';
$body .= '<div class="actions" style="margin-top:0">';
$body .= '<a class="btn primary" href="' . AppContext::e($ctx->url('change_password.html', ['add_user' => 1])) . '#add-user-form">Dodaj użytkownika</a>';
if ($showCreateUserForm) {
$body .= '<a class="btn" href="' . AppContext::e($ctx->url('change_password.html')) . '">Anuluj</a>';
}
$body .= '</div>';
if ($selectedRole !== '') {
$body .= '<div class="actions" style="margin-top:0">';
$body .= '<form method="get" action="' . AppContext::e($ctx->url('change_password.html')) . '" class="actions" style="margin-top:0">';
$body .= '<label style="margin:0">Użytkownik: <select name="role" style="width:auto;display:inline-block;margin-left:8px">' . $roleOptions . '</select></label>';
$body .= '<button class="btn" type="submit">Przełącz</button>';
$body .= '</form>';
$body .= '<a class="btn danger-fill" href="' . AppContext::e($ctx->url('change_password.html', ['role' => $selectedRole, 'delete_role' => $selectedRole])) . '">Usuń użytkownika</a>';
$body .= '</div>';
} else {
$body .= '<span class="muted">Brak użytkowników MySQL. Dodaj pierwszego użytkownika.</span>';
}
$body .= '</div>';
$body .= '<p class="section-text">Szczegółowe informacje o użytkowniku.</p>';
if ($selectedRole !== '') {
$body .= '<table><tbody>';
$body .= '<tr><td><strong>Nazwa użytkownika</strong><br>' . AppContext::e($selectedRole) . '</td><td><strong>Bazy danych</strong><br>' . AppContext::e((string)count($assignedDbs)) . '</td>';
if ($showRemoteHosts) {
$body .= '<td><strong>Dozwolone hosty</strong><br>' . AppContext::e((string)$hostsCount) . '</td>';
} else {
$fixedHostSummary = implode(', ', array_keys($fixedHosts));
$body .= '<td><strong>Dozwolone hosty</strong><br>' . AppContext::e($fixedHostSummary) . '</td>';
}
$body .= '</tr></tbody></table>';
} else {
$body .= '<p class="muted">Aby zarządzać użytkownikami najpierw utwórz użytkownika MySQL.</p>';
}
$body .= '</section>';
if ($deletePromptRole !== '') {
$cancelUrl = $ctx->url('change_password.html', $selectedRole !== '' ? ['role' => $selectedRole] : []);
$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ąć użytkownika <strong>' . AppContext::e($deletePromptRole) . '</strong>?</div>';
if (count($deletePromptAssignedDbs) === 1) {
$body .= '<div class="alert alert-err" style="margin:0 0 10px">Uwaga użytkownik <strong>' . AppContext::e($deletePromptRole) . '</strong> jest przypisany do następującej bazy danych <strong>' . AppContext::e($deletePromptAssignedDbs[0]) . '</strong>.</div>';
} elseif (count($deletePromptAssignedDbs) > 1) {
$body .= '<div class="alert alert-err" style="margin:0 0 10px">Uwaga użytkownik <strong>' . AppContext::e($deletePromptRole) . '</strong> jest przypisany do następujących baz danych:</div>';
$body .= '<ul class="list-clean" style="margin:0 0 12px 18px">';
foreach ($deletePromptAssignedDbs as $assignedDbName) {
$body .= '<li>' . AppContext::e($assignedDbName) . '</li>';
}
$body .= '</ul>';
}
$body .= '<form method="post">';
$body .= $ctx->csrfField('delete_user_submit');
$body .= '<input type="hidden" name="form_action" value="delete_user_confirm">';
$body .= '<input type="hidden" name="delete_role" value="' . AppContext::e($deletePromptRole) . '">';
$body .= '<div class="actions">';
$body .= '<button class="danger-fill" type="submit">Potwierdź usunięcie użytkownika</button>';
$body .= '<a class="btn" href="' . AppContext::e($cancelUrl) . '">Anuluj</a>';
$body .= '</div>';
$body .= '</form>';
$body .= '</section>';
}
if ($showCreateUserForm) {
$body .= '<section class="panel" id="add-user-form" style="margin-top:14px">';
$body .= '<h3>Utwórz nowego użytkownika MySQL</h3>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('create_user_inline_submit');
$body .= '<input type="hidden" name="form_action" value="create_user_inline">';
$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" name="create_role_name" value="' . AppContext::e($createRoleSuffixInput) . '" autocomplete="off" required></div></div>';
$body .= '<div><label>Hasło użytkownika</label>';
$body .= '<div class="input-with-tools"><input type="password" id="create-role-password" name="create_password" value="' . AppContext::e($createPasswordInput) . '" autocomplete="new-password" required>';
$body .= '<span class="field-tools">';
$body .= '<button type="button" class="tool-btn" data-generate-target="create-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="create-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 .= '</div>';
$body .= '</div>';
$body .= '<details class="details-advanced">';
$body .= '<summary>Przypisz do baz danych (opcjonalnie)</summary>';
if (empty($allDbs)) {
$body .= '<p class="muted">Brak baz do przypisania.</p>';
} else {
$body .= '<div class="db-choice-grid">';
foreach ($allDbs as $dbName) {
$checked = isset($createSelectedDbSet[$dbName]) ? ' checked' : '';
$body .= '<label class="db-choice-card"><input type="checkbox" name="create_user_databases[]" value="' . AppContext::e($dbName) . '"' . $checked . '><span>' . AppContext::e($dbName) . '</span></label>';
}
$body .= '</div>';
}
$body .= '<p class="muted" style="margin-top:8px">Możesz pozostawić użytkownika bez przypisania do baz i nadać dostęp później.</p>';
$body .= '</details>';
$body .= '<div class="actions"><button class="primary" type="submit">Utwórz użytkownika</button></div>';
$body .= '</form>';
$body .= '</section>';
}
if ($selectedRole !== '') {
$body .= '<section class="panel" style="margin-top:14px">';
$body .= '<h3>Zarządzanie hasłami</h3>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('user_manage_submit');
$body .= '<input type="hidden" name="role_name" value="' . AppContext::e($selectedRole) . '">';
$body .= '<input type="hidden" name="form_action" value="change_password">';
$body .= '<label>Nowe hasło</label>';
$body .= '<div class="input-with-tools"><input type="password" id="change-role-password" name="password" placeholder="Wpisz nowe hasło" autocomplete="new-password" required>';
$body .= '<span class="field-tools">';
$body .= '<button type="button" class="tool-btn" data-generate-target="change-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="change-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 .= '<div class="actions"><button class="primary" type="submit">Zmień hasło</button></div>';
$body .= '</form>';
$body .= '</section>';
$body .= '<section class="panel" style="margin-top:14px">';
$body .= '<h3>Dostęp do baz danych</h3>';
$body .= '<table><thead><tr><th>Nazwa bazy danych</th><th>Przywileje</th><th>Akcje</th></tr></thead><tbody>' . $dbRows . '</tbody></table>';
$body .= '<form method="post" style="margin-top:10px">';
$body .= $ctx->csrfField('user_manage_submit');
$body .= '<input type="hidden" name="role_name" value="' . AppContext::e($selectedRole) . '">';
$body .= '<input type="hidden" name="form_action" value="grant_db">';
$body .= '<label>Przyznaj dostęp do kolejnej bazy danych</label>';
$body .= '<select name="db_name">' . $dbOptions . '</select>';
$body .= '<input type="hidden" name="privileges[]" value="ALL">';
$body .= '<div class="actions"><button class="btn" type="submit">+ Przyznaj pełny dostęp</button></div>';
$body .= '</form>';
$body .= '</section>';
if ($showRemoteHosts) {
$fixedHostsHelp = '`localhost` jest dodawany automatycznie.';
if (count($fixedHosts) > 1) {
$fixedHostsHelp .= ' Przy zdalnym serwerze MySQL plugin dodaje tez adres IP serwera DirectAdmin.';
}
$body .= '<section class="panel" style="margin-top:14px">';
$body .= '<h3>Dozwolone hosty</h3>';
$body .= '<p class="section-text">Lista źródłowych adresów IP, które mogą łączyć się z bazą przy użyciu poświadczeń użytkownika. ' . $fixedHostsHelp . '</p>';
$body .= '<table><thead><tr><th>Host</th><th>Komentarz</th><th>Akcje</th></tr></thead><tbody>' . $hostsRows . '</tbody></table>';
$body .= '<form method="post" style="margin-top:10px">';
$body .= $ctx->csrfField('user_manage_submit');
$body .= '<input type="hidden" name="role_name" value="' . AppContext::e($selectedRole) . '">';
$body .= '<input type="hidden" name="form_action" value="add_host">';
$body .= '<div class="row">';
$body .= '<div><label>Zezwalaj na dostęp z</label><input type="text" name="host" placeholder="203.0.113.10"></div>';
$body .= '<div><label>Komentarz</label><input type="text" name="host_note" placeholder="np. biuro / serwer aplikacji"></div>';
$body .= '</div>';
$body .= '<div class="actions"><button class="btn" type="submit">+ Dodaj host</button></div>';
$body .= '</form>';
$body .= '</section>';
}
}
$ctx->renderPage('Zarządzaj użytkownikami', $body, $ok, $errors);
};
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
if ($ctx->isPost()) {
if ($ctx->postString('form_action') === '') {
$_POST['form_action'] = 'create_database';
}
if ($ctx->postString('db_suffix') === '' && $ctx->postString('db_name') !== '') {
$_POST['db_suffix'] = $ctx->postString('db_name');
}
$handler = require __DIR__ . '/index.php';
$handler($ctx);
return;
}
Http::redirect($ctx->url('index.html'));
};
+150
View File
@@ -0,0 +1,150 @@
<?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());
$databases = $ctx->mysql->listDatabasesForUser($daUser);
$dbNames = array_map(static fn(array $db): string => $db['name'], $databases);
if ($ctx->isPost()) {
try {
$ctx->requireCsrf('create_user_submit');
$role = NamePolicy::normalizeRoleName($ctx->postString('role_name'), $prefix);
if ($ctx->mysql->roleExists($role)) {
throw new RuntimeException('Użytkownik MySQL już istnieje: ' . $role);
}
$password = $ctx->postString('password');
if (strlen($password) < 12) {
throw new RuntimeException('Hasło użytkownika musi mieć co najmniej 12 znaków.');
}
$selectedDbsRaw = $ctx->postArray('databases');
$selectedDbs = [];
foreach ($selectedDbsRaw as $db) {
$normalizedDb = NamePolicy::normalizeDatabaseName($db, $prefix);
if (!in_array($normalizedDb, $dbNames, true)) {
throw new RuntimeException('Nieprawidłowa baza do przypisania: ' . $normalizedDb);
}
$selectedDbs[] = $normalizedDb;
}
$selectedDbs = array_values(array_unique($selectedDbs));
$privileges = NamePolicy::sanitizePrivileges($ctx->postArray('privileges'));
if (empty($privileges)) {
$privileges = NamePolicy::defaultPrivileges();
}
$hostMap = [];
if ($showRemoteHosts) {
$remoteHostRaw = trim($ctx->postString('remote_host'));
if ($remoteHostRaw !== '') {
$remoteHost = NamePolicy::normalizeRemoteIpv4Host($remoteHostRaw);
$remoteComment = NamePolicy::normalizeHostComment($ctx->postString('remote_comment'));
$hostMap[$remoteHost] = $remoteComment;
}
}
$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.');
}
$hostEntries = [];
foreach ($hostMap as $host => $note) {
$hostEntries[] = ['host' => $host, 'note' => $note];
}
$ctx->mysql->createRole($role, $password);
try {
$ctx->mysql->replaceRoleHostsWithNotes($daUser, $role, $hostEntries);
foreach ($selectedDbs as $dbName) {
$ctx->mysql->grantPrivileges($dbName, $role, $privileges);
}
$sync = $ctx->mysql->syncHostRules();
if (!$sync['ok']) {
$errors[] = 'Użytkownik został utworzony, ale synchronizacja hostów MySQL nie powiodła się: ' . $sync['output'];
}
$ok[] = 'Utworzono użytkownika ' . $role . ' i przypisano ' . count($selectedDbs) . ' baz(y).';
} catch (Throwable $inner) {
try {
foreach ($selectedDbs as $dbName) {
$ctx->mysql->revokePrivileges($dbName, $role);
}
$ctx->mysql->deleteRoleHosts($daUser, $role);
$ctx->mysql->dropRole($role);
} catch (Throwable $ignored) {
}
throw $inner;
}
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
$dbCheckboxes = '';
foreach ($dbNames as $dbName) {
$dbCheckboxes .= '<label class="inline"><input type="checkbox" name="databases[]" value="' . AppContext::e($dbName) . '"> ' . AppContext::e($dbName) . '</label>';
}
if ($dbCheckboxes === '') {
$dbCheckboxes = '<p class="muted">Brak baz do przypisania.</p>';
}
$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>';
}
$remoteFields = '';
if ($showRemoteHosts) {
$remoteFields .= '<div class="row">';
$remoteFields .= '<div><label>Dodatkowy host IPv4</label><input type="text" name="remote_host" placeholder="203.0.113.10"></div>';
$remoteFields .= '<div><label>Komentarz hosta</label><input type="text" name="remote_comment" placeholder="np. Laptop administratora"></div>';
$remoteFields .= '</div>';
} else {
$remoteFields .= '<p class="muted">Hosty zdalne są wyłączone w konfiguracji pluginu (`allow_remote_hosts=0`).</p>';
}
$body = '';
$body .= '<form method="post">';
$body .= $ctx->csrfField('create_user_submit');
$body .= '<div class="row">';
$body .= '<div><label>Nazwa użytkownika MySQL</label><input type="text" name="role_name" placeholder="' . AppContext::e($prefix) . 'app_user" required></div>';
$body .= '<div><label>Hasło</label><input type="password" name="password" placeholder="min. 12 znaków" required></div>';
$body .= '</div>';
$defaultAccessText = '`localhost` jest dodawany automatycznie.';
if (count($fixedHosts) > 1) {
$defaultAccessText .= ' Przy zdalnym serwerze MySQL plugin dodaje tez adres IP serwera DirectAdmin.';
}
$body .= '<div class="row">';
$body .= '<div><label>Domyślny dostęp</label><p class="muted">' . $defaultAccessText . '</p></div>';
$body .= '<div><label>Przypisz do baz</label><div>' . $dbCheckboxes . '</div></div>';
$body .= '</div>';
$body .= $remoteFields;
$body .= '<div class="panel"><h3>Uprawnienia dla przypisywanych baz</h3><div class="privileges-group" data-privileges-group>' . $privCheckboxes . '</div></div>';
$body .= '<div class="actions"><button class="primary" type="submit">Utwórz użytkownika</button></div>';
$body .= '</form>';
$ctx->renderPage('Tworzenie Użytkownika MySQL', $body, $ok, $errors);
};
+170
View File
@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$ok = [];
$errors = [];
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$availableDbs = array_map(static fn(array $db): string => $db['name'], $ctx->mysql->listDatabasesForUser($daUser));
$selectedDatabases = $ctx->postArray('databases');
if (empty($selectedDatabases)) {
$queryDb = $ctx->queryString('db');
if ($queryDb !== '') {
$selectedDatabases = [$queryDb];
}
}
$selectedDatabases = array_values(array_unique(array_filter($selectedDatabases, static fn(string $v): bool => $v !== '')));
$confirmDelete = $ctx->postString('confirm_delete') === '1';
if ($ctx->isPost() && !$confirmDelete) {
try {
$ctx->requireCsrf('select_delete_databases');
} catch (Throwable $e) {
$errors[] = $e->getMessage();
$selectedDatabases = [];
}
}
if ($ctx->isPost() && $confirmDelete) {
try {
$ctx->requireCsrf('delete_databases_confirm');
if (empty($selectedDatabases)) {
throw new RuntimeException('Nie wybrano baz danych do usunięcia.');
}
$deleteRelatedUsers = $ctx->postString('delete_related_users') === '1';
$deletedDbs = [];
$deletedUsers = [];
$keptUsers = [];
foreach ($selectedDatabases as $rawDb) {
try {
$dbName = NamePolicy::normalizeDatabaseName($rawDb, $prefix);
if (!in_array($dbName, $availableDbs, true)) {
$errors[] = 'Pominięto bazę spoza konta: ' . $dbName;
continue;
}
$users = $ctx->mysql->listDatabaseUsers($daUser, $dbName);
$users = array_values(array_unique($users));
foreach ($users as $roleName) {
$ctx->mysql->revokePrivileges($dbName, $roleName);
}
$ctx->mysql->dropDatabase($dbName);
$deletedDbs[] = $dbName;
if ($deleteRelatedUsers) {
foreach ($users as $roleName) {
$assigned = $ctx->mysql->roleAssignedDatabases($roleName, $daUser);
$owned = $ctx->mysql->roleOwnedDatabases($roleName, $daUser);
if (empty($assigned) && empty($owned)) {
try {
$ctx->mysql->deleteRoleHosts($daUser, $roleName);
$ctx->mysql->dropRole($roleName);
$deletedUsers[] = $roleName;
} catch (Throwable $dropErr) {
$keptUsers[] = $roleName . ' (' . $dropErr->getMessage() . ')';
}
} else {
$keptUsers[] = $roleName . ' (ma nadal przypisane bazy)';
}
}
}
} catch (Throwable $inner) {
$errors[] = $rawDb . ': ' . $inner->getMessage();
}
}
$sync = $ctx->mysql->syncHostRules();
if (!$sync['ok']) {
$errors[] = 'Operacja wykonana, ale synchronizacja hostów MySQL nie powiodła się: ' . $sync['output'];
}
if (!empty($deletedDbs)) {
$ok[] = 'Usunięto bazy: ' . implode(', ', array_values(array_unique($deletedDbs)));
}
if (!empty($deletedUsers)) {
$ok[] = 'Usunięto użytkowników powiązanych z usuniętymi bazami: ' . implode(', ', array_values(array_unique($deletedUsers)));
}
if (!empty($keptUsers)) {
$errors[] = 'Użytkownicy pozostawieni: ' . implode('; ', array_values(array_unique($keptUsers)));
}
$selectedDatabases = [];
$availableDbs = array_map(static fn(array $db): string => $db['name'], $ctx->mysql->listDatabasesForUser($daUser));
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
if (empty($selectedDatabases)) {
$rows = '';
foreach ($availableDbs as $dbName) {
$users = $ctx->mysql->listDatabaseUsers($daUser, $dbName);
$rows .= '<tr>';
$rows .= '<td><input type="checkbox" name="databases[]" value="' . AppContext::e($dbName) . '"></td>';
$rows .= '<td>' . AppContext::e($dbName) . '</td>';
$rows .= '<td>' . AppContext::e(implode(', ', $users)) . '</td>';
$rows .= '</tr>';
}
if ($rows === '') {
$rows = '<tr><td colspan="3" class="muted">Brak baz do usunięcia.</td></tr>';
}
$body = '';
$body .= '<form method="post">';
$body .= $ctx->csrfField('select_delete_databases');
$body .= '<table><thead><tr><th></th><th>Baza</th><th>Przypisani użytkownicy</th></tr></thead><tbody>' . $rows . '</tbody></table>';
$body .= '<div class="actions"><button class="danger" type="submit">Usuń zaznaczone bazy</button></div>';
$body .= '</form>';
$ctx->renderPage('Usuwanie Baz Danych', $body, $ok, $errors);
return;
}
$confirmRows = '';
foreach ($selectedDatabases as $rawDb) {
try {
$dbName = NamePolicy::normalizeDatabaseName($rawDb, $prefix);
$users = $ctx->mysql->listDatabaseUsers($daUser, $dbName);
$confirmRows .= '<tr>';
$confirmRows .= '<td>' . AppContext::e($dbName) . '<input type="hidden" name="databases[]" value="' . AppContext::e($dbName) . '"></td>';
$confirmRows .= '<td>' . AppContext::e(implode(', ', $users)) . '</td>';
$confirmRows .= '</tr>';
} catch (Throwable $e) {
$errors[] = $rawDb . ': ' . $e->getMessage();
}
}
if ($confirmRows === '') {
$ctx->renderPage('Usuwanie Baz Danych', '<p class="muted">Brak poprawnych baz do usunięcia.</p>', $ok, $errors);
return;
}
$body = '';
$body .= '<p>Potwierdź usunięcie baz danych. Operacja jest nieodwracalna.</p>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('delete_databases_confirm');
$body .= '<input type="hidden" name="confirm_delete" value="1">';
$body .= '<table><thead><tr><th>Baza</th><th>Przypisani użytkownicy</th></tr></thead><tbody>' . $confirmRows . '</tbody></table>';
$body .= '<p><label class="inline"><input type="checkbox" name="delete_related_users" value="1"> Usuń także użytkowników przypisanych do usuwanych baz (jeżeli po usunięciu baz nie mają już innych przypisań)</label></p>';
$body .= '<div class="actions">';
$body .= '<button class="danger" type="submit">Potwierdź usunięcie</button>';
$body .= '<a class="btn" href="' . AppContext::e($ctx->url('delete_databases.html')) . '">Anuluj</a>';
$body .= '</div>';
$body .= '</form>';
$ctx->renderPage('Potwierdzenie Usuwania Baz Danych', $body, $ok, $errors);
};
+158
View File
@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$ok = [];
$errors = [];
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$availableRoles = array_map(static fn(array $r): string => $r['name'], $ctx->mysql->listRolesForUser($daUser));
$selectedRoles = $ctx->postArray('roles');
if (empty($selectedRoles)) {
$queryRole = $ctx->queryString('role');
if ($queryRole !== '') {
$selectedRoles = [$queryRole];
}
}
$selectedRoles = array_values(array_unique(array_filter($selectedRoles, static fn(string $v): bool => $v !== '')));
$confirmDelete = $ctx->postString('confirm_delete') === '1';
if ($ctx->isPost() && !$confirmDelete) {
try {
$ctx->requireCsrf('select_delete_users');
} catch (Throwable $e) {
$errors[] = $e->getMessage();
$selectedRoles = [];
}
}
if ($ctx->isPost() && $confirmDelete) {
try {
$ctx->requireCsrf('delete_users_confirm');
if (empty($selectedRoles)) {
throw new RuntimeException('Nie wybrano użytkowników do usunięcia.');
}
$deleted = [];
$skipped = [];
foreach ($selectedRoles as $rawRole) {
try {
$role = NamePolicy::normalizeRoleName($rawRole, $prefix);
if (!in_array($role, $availableRoles, true)) {
$skipped[] = $role . ' (nie istnieje)';
continue;
}
$owned = $ctx->mysql->roleOwnedDatabases($role, $daUser);
if (!empty($owned)) {
$skipped[] = $role . ' (właściciel baz: ' . implode(', ', $owned) . ')';
continue;
}
$assigned = $ctx->mysql->roleAssignedDatabases($role, $daUser);
foreach ($assigned as $dbName) {
$owner = $ctx->mysql->getDatabaseOwner($dbName);
if ($owner !== null && $owner !== $role) {
$ctx->mysql->reassignOwnedObjects($dbName, $role, $owner);
}
$ctx->mysql->revokePrivileges($dbName, $role);
}
$ctx->mysql->deleteRoleHosts($daUser, $role);
$ctx->mysql->dropRole($role);
$deleted[] = $role;
} catch (Throwable $inner) {
$skipped[] = $rawRole . ' (' . $inner->getMessage() . ')';
}
}
$sync = $ctx->mysql->syncHostRules();
if (!$sync['ok']) {
$errors[] = 'Usuwanie wykonane, ale synchronizacja hostów MySQL nie powiodła się: ' . $sync['output'];
}
if (!empty($deleted)) {
$ok[] = 'Usunięto użytkowników: ' . implode(', ', $deleted);
}
if (!empty($skipped)) {
$errors[] = 'Nie usunięto: ' . implode('; ', $skipped);
}
$selectedRoles = [];
$availableRoles = array_map(static fn(array $r): string => $r['name'], $ctx->mysql->listRolesForUser($daUser));
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
if (empty($selectedRoles)) {
$rows = '';
foreach ($availableRoles as $roleName) {
$assigned = $ctx->mysql->roleAssignedDatabases($roleName, $daUser);
$owned = $ctx->mysql->roleOwnedDatabases($roleName, $daUser);
$rows .= '<tr>';
$rows .= '<td><input type="checkbox" name="roles[]" value="' . AppContext::e($roleName) . '"></td>';
$rows .= '<td>' . AppContext::e($roleName) . '</td>';
$rows .= '<td>' . AppContext::e(implode(', ', $assigned)) . '</td>';
$rows .= '<td>' . AppContext::e(implode(', ', $owned)) . '</td>';
$rows .= '</tr>';
}
if ($rows === '') {
$rows = '<tr><td colspan="4" class="muted">Brak użytkowników do usunięcia.</td></tr>';
}
$body = '';
$body .= '<form method="post">';
$body .= $ctx->csrfField('select_delete_users');
$body .= '<table><thead><tr><th></th><th>Użytkownik</th><th>Przypisane bazy</th><th>Bazy, których jest właścicielem</th></tr></thead><tbody>' . $rows . '</tbody></table>';
$body .= '<div class="actions"><button class="danger" type="submit">Usuń zaznaczonych użytkowników</button></div>';
$body .= '</form>';
$ctx->renderPage('Usuwanie Użytkowników', $body, $ok, $errors);
return;
}
$confirmRows = '';
foreach ($selectedRoles as $rawRole) {
try {
$role = NamePolicy::normalizeRoleName($rawRole, $prefix);
$assigned = $ctx->mysql->roleAssignedDatabases($role, $daUser);
$owned = $ctx->mysql->roleOwnedDatabases($role, $daUser);
$confirmRows .= '<tr>';
$confirmRows .= '<td>' . AppContext::e($role) . '<input type="hidden" name="roles[]" value="' . AppContext::e($role) . '"></td>';
$confirmRows .= '<td>' . AppContext::e(implode(', ', $assigned)) . '</td>';
$confirmRows .= '<td>' . AppContext::e(implode(', ', $owned)) . '</td>';
$confirmRows .= '</tr>';
} catch (Throwable $e) {
$errors[] = $rawRole . ': ' . $e->getMessage();
}
}
if ($confirmRows === '') {
$ctx->renderPage('Usuwanie Użytkowników', '<p class="muted">Brak poprawnych użytkowników do usunięcia.</p>', $ok, $errors);
return;
}
$body = '';
$body .= '<p>Potwierdź usunięcie zaznaczonych użytkowników MySQL. Operacja jest nieodwracalna.</p>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('delete_users_confirm');
$body .= '<input type="hidden" name="confirm_delete" value="1">';
$body .= '<table><thead><tr><th>Użytkownik</th><th>Przypisane bazy</th><th>Bazy, których jest właścicielem</th></tr></thead><tbody>';
$body .= $confirmRows;
$body .= '</tbody></table>';
$body .= '<div class="actions">';
$body .= '<button class="danger" type="submit">Potwierdź usunięcie</button>';
$body .= '<a class="btn" href="' . AppContext::e($ctx->url('delete_users.html')) . '">Anuluj</a>';
$body .= '</div>';
$body .= '</form>';
$ctx->renderPage('Potwierdzenie Usuwania Użytkowników', $body, $ok, $errors);
};
+111
View File
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$pluginErrorLog = PLUGIN_ROOT . '/error.log';
$rawMode = defined('DA_MYSQL_RAW_MODE') && DA_MYSQL_RAW_MODE === true;
$log = static function (string $message) use ($pluginErrorLog, $ctx): void {
$entry = sprintf(
"[%s] DOWNLOAD_DIRECT action=%s user=%s remote=%s method=%s uri=%s %s\n",
date('c'),
$ctx->action(),
$ctx->daUser->username(),
Http::server('REMOTE_ADDR'),
Http::method(),
Http::server('REQUEST_URI'),
$message
);
@error_log($entry, 3, $pluginErrorLog);
};
try {
if (!$ctx->daUser->hasPluginAccess()) {
throw new RuntimeException('Brak dostępu do pluginu.');
}
$queue = new BackupQueueService($ctx->settings, $ctx->daUser);
$jobId = trim($ctx->queryString('job'));
$backupPath = trim($ctx->queryString('path'));
$downloadPath = '';
$logLabel = '';
if ($jobId !== '') {
$logLabel = 'job_id=' . $jobId;
$downloadPath = $queue->resolveBackupTargetFileForCurrentUser($jobId);
} elseif ($backupPath !== '') {
$logLabel = 'path=' . $backupPath;
$downloadPath = $queue->resolveUserBackupFileForCurrentUser($backupPath);
} else {
throw new RuntimeException('Brak parametru job lub path.');
}
// Reuse streaming logic from index handler (simplified)
$log('start ' . $logLabel);
if (!is_file($downloadPath) || !is_readable($downloadPath)) {
throw new RuntimeException('Plik backupu nie istnieje lub brak odczytu.');
}
$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);
$size = @filesize($downloadPath);
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 ($rawMode) {
$statusLine = 'HTTP/1.1 200 OK';
echo $statusLine . "\r\n";
echo 'Content-Type: ' . $contentType . "\r\n";
echo 'Content-Disposition: attachment; filename="' . str_replace('"', '', $downloadName) . "\"\r\n";
echo "X-Content-Type-Options: nosniff\r\n";
echo "Cache-Control: no-store, no-cache, must-revalidate\r\n";
echo "Pragma: no-cache\r\n";
if (is_int($size) && $size > 0) {
echo 'Content-Length: ' . (string)$size . "\r\n";
}
echo "\r\n";
} else {
if (headers_sent($hsFile, $hsLine)) {
$log('headers_sent ' . $logLabel . ' file=' . $hsFile . ' line=' . (string)$hsLine);
throw new RuntimeException('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($size) && $size > 0) {
header('Content-Length: ' . (string)$size);
}
}
$bytes = @readfile($downloadPath);
$log('done ' . $logLabel . ' bytes=' . (string)$bytes);
exit;
} catch (Throwable $e) {
$log('error message=' . $e->getMessage());
if ($rawMode) {
echo "HTTP/1.1 404 Not Found\r\n";
echo "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
echo 'Nie można pobrać backupu: ' . $e->getMessage();
} else {
http_response_code(404);
header('Content-Type: text/plain; charset=UTF-8');
echo 'Nie można pobrać backupu: ' . $e->getMessage();
}
exit;
}
};
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$queue = new BackupQueueService($ctx->settings, $ctx->daUser);
$root = '/home/' . $ctx->daUser->username();
if (FilePicker::handleDirList($ctx, $queue, $root)) {
return;
}
if (FilePicker::handleDirCreate($ctx, $root, $ctx->daUser->username())) {
return;
}
http_response_code(404);
header('Content-Type: text/plain; charset=UTF-8');
echo 'Not Found';
};
+792
View File
@@ -0,0 +1,792 @@
<?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);
};
+162
View File
@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
if (!$ctx->settings->allowRemoteHosts()) {
$ctx->renderFatal('Zarządzanie hostami zdalnymi jest wyłączone (`allow_remote_hosts=0`).', 403);
return;
}
$ok = [];
$errors = [];
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$fixedHosts = array_flip($ctx->mysql->requiredAccessHosts());
if (!$ctx->isPost()) {
$sync = $ctx->mysql->syncHostRules();
if (!$sync['ok']) {
$errors[] = 'Synchronizacja zdalnych hostów MySQL nie powiodła się: ' . $sync['output'];
}
}
$roleNames = array_map(static fn(array $r): string => $r['name'], $ctx->mysql->listRolesForUser($daUser));
$selectedRole = $ctx->queryString('role');
if ($ctx->isPost()) {
$selectedRole = $ctx->postString('role_name');
try {
$ctx->requireCsrf('manage_hosts_submit');
$selectedRole = NamePolicy::normalizeRoleName($selectedRole, $prefix);
if (!in_array($selectedRole, $roleNames, true)) {
throw new RuntimeException('Użytkownik MySQL nie należy do konta DA: ' . $selectedRole);
}
$currentEntries = $ctx->mysql->getRoleHostEntries($daUser, $selectedRole);
$hostMap = [];
foreach ($currentEntries as $entry) {
$hostMap[$entry['host']] = $entry['note'];
}
$removeHosts = $ctx->postArray('remove_hosts');
$removeHosts = array_values(array_unique($removeHosts));
foreach ($removeHosts as $rawHost) {
$host = trim($rawHost);
if ($host === '' || isset($fixedHosts[$host])) {
continue;
}
if (array_key_exists($host, $hostMap)) {
unset($hostMap[$host]);
}
}
$addHostRaw = trim($ctx->postString('add_host'));
if ($addHostRaw !== '') {
$addHost = NamePolicy::normalizeRemoteIpv4Host($addHostRaw);
$addComment = NamePolicy::normalizeHostComment($ctx->postString('add_comment'));
$hostMap[$addHost] = $addComment;
}
$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[] = 'Hosty zapisane, ale synchronizacja hostów MySQL nie powiodła się: ' . $sync['output'];
}
$ok[] = 'Zaktualizowano hosty dostępu dla użytkownika ' . $selectedRole . '.';
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
if ($selectedRole !== '') {
try {
$selectedRole = NamePolicy::normalizeRoleName($selectedRole, $prefix);
} catch (Throwable $e) {
$selectedRole = '';
}
}
if ($selectedRole === '' && !empty($roleNames)) {
$selectedRole = $roleNames[0];
}
$currentEntries = [];
if ($selectedRole !== '') {
$currentEntries = $ctx->mysql->getRoleHostEntries($daUser, $selectedRole);
}
$roleOptions = '<option value="">-- wybierz użytkownika --</option>';
foreach ($roleNames as $roleName) {
$sel = ($roleName === $selectedRole) ? ' selected' : '';
$roleOptions .= '<option value="' . AppContext::e($roleName) . '"' . $sel . '>' . AppContext::e($roleName) . '</option>';
}
$hostRows = '';
if (empty($currentEntries)) {
$hostRows = '<tr><td colspan="3" class="muted">Brak zapisanych hostów.</td></tr>';
} else {
foreach ($currentEntries as $entry) {
$host = $entry['host'];
$note = $entry['note'];
$canRemove = !isset($fixedHosts[$host]);
$hostRows .= '<tr>';
$hostRows .= '<td>' . AppContext::e($host) . '</td>';
$hostRows .= '<td>' . AppContext::e($note) . '</td>';
if ($canRemove) {
$hostRows .= '<td><label class="inline"><input type="checkbox" name="remove_hosts[]" value="' . AppContext::e($host) . '"> usuń</label></td>';
} else {
$hostRows .= '<td><span class="muted">host stały</span></td>';
}
$hostRows .= '</tr>';
}
}
$body = '';
$body .= '<form method="post">';
$body .= $ctx->csrfField('manage_hosts_submit');
$body .= '<div class="row">';
$body .= '<div><label>Użytkownik MySQL</label><select name="role_name" required>' . $roleOptions . '</select></div>';
$body .= '<div><label>Dodaj IPv4</label><input type="text" name="add_host" placeholder="198.51.100.12"></div>';
$body .= '</div>';
$infoText = 'Dozwolone są wyłącznie pojedyncze adresy IPv4, bez CIDR i bez nazw hostów.';
if (count($fixedHosts) > 1) {
$infoText .= ' `localhost` oraz adres IP serwera DirectAdmin sa hostami stalymi i nie mozna ich usunac.';
} else {
$infoText .= ' `localhost` jest hostem stalym i nie mozna go usunac.';
}
$body .= '<div class="row">';
$body .= '<div><label>Komentarz do nowego hosta</label><input type="text" name="add_comment" placeholder="np. serwer aplikacyjny PROD"></div>';
$body .= '<div><label>Informacja</label><p class="muted">' . $infoText . '</p></div>';
$body .= '</div>';
$body .= '<div class="panel"><h3>Aktualne hosty</h3>';
$body .= '<table><thead><tr><th>Host</th><th>Komentarz</th><th>Akcja</th></tr></thead><tbody>' . $hostRows . '</tbody></table>';
$body .= '</div>';
$body .= '<div class="actions"><button class="primary" type="submit">Zapisz hosty</button></div>';
$body .= '</form>';
$ctx->renderPage('Hosty Zdalne Użytkownika', $body, $ok, $errors);
};
+202
View File
@@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$ok = [];
$errors = [];
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$dbNames = array_map(static fn(array $db): string => $db['name'], $ctx->mysql->listDatabasesForUser($daUser));
$roleNames = array_map(static fn(array $r): string => $r['name'], $ctx->mysql->listRolesForUser($daUser));
if (empty($dbNames)) {
$ctx->renderPage('Zarządzaj bazą danych', '<div class="panel"><p class="muted">Brak baz danych do zarządzania.</p></div>', $ok, $errors);
return;
}
$selectedDb = $ctx->postString('db_name');
if ($selectedDb === '') {
$selectedDb = $ctx->queryString('db');
}
if ($selectedDb === '') {
$selectedDb = $dbNames[0];
}
try {
$selectedDb = NamePolicy::normalizeDatabaseName($selectedDb, $prefix);
} catch (Throwable $e) {
$selectedDb = $dbNames[0];
}
if (!in_array($selectedDb, $dbNames, true)) {
$selectedDb = $dbNames[0];
}
$selectedRole = $ctx->postString('role_name');
if ($selectedRole === '') {
$selectedRole = $ctx->queryString('role');
}
if ($ctx->isPost()) {
try {
$ctx->requireCsrf('modify_privileges_submit');
$selectedDb = NamePolicy::normalizeDatabaseName($ctx->postString('db_name', $selectedDb), $prefix);
if (!in_array($selectedDb, $dbNames, true)) {
throw new RuntimeException('Baza nie należy do konta DA: ' . $selectedDb);
}
$action = $ctx->postString('form_action', 'save_privileges');
if ($action === 'revoke_user') {
$role = NamePolicy::normalizeRoleName($ctx->postString('role_name'), $prefix);
if (!in_array($role, $roleNames, true)) {
throw new RuntimeException('Nieprawidłowy użytkownik MySQL: ' . $role);
}
$ctx->mysql->revokePrivileges($selectedDb, $role);
$selectedRole = $role;
$ok[] = 'Odebrano dostęp użytkownikowi ' . $role . ' do bazy ' . $selectedDb . '.';
} elseif ($action === 'grant_user') {
$role = NamePolicy::normalizeRoleName($ctx->postString('role_name'), $prefix);
if (!in_array($role, $roleNames, true)) {
throw new RuntimeException('Nieprawidłowy użytkownik MySQL: ' . $role);
}
$privileges = NamePolicy::sanitizePrivileges($ctx->postArray('grant_privileges'));
if (empty($privileges)) {
$privileges = NamePolicy::defaultPrivileges();
}
$ctx->mysql->grantPrivileges($selectedDb, $role, $privileges);
$selectedRole = $role;
$ok[] = 'Przyznano dostęp użytkownikowi ' . $role . ' do bazy ' . $selectedDb . '.';
} else {
$role = NamePolicy::normalizeRoleName($ctx->postString('role_name'), $prefix);
if (!in_array($role, $roleNames, true)) {
throw new RuntimeException('Nieprawidłowy użytkownik MySQL: ' . $role);
}
$privileges = NamePolicy::sanitizePrivileges($ctx->postArray('privileges'));
if (empty($privileges)) {
throw new RuntimeException('Wybierz przynajmniej jedno uprawnienie.');
}
$ctx->mysql->grantPrivileges($selectedDb, $role, $privileges);
$selectedRole = $role;
$ok[] = 'Zaktualizowano uprawnienia użytkownika ' . $role . '.';
}
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
$dbInfo = $ctx->mysql->getDatabaseInfo($selectedDb);
$dbStats = $ctx->mysql->getDatabaseObjectStats($selectedDb);
$dbUsers = $ctx->mysql->listDatabaseUsers($daUser, $selectedDb);
if ($selectedRole !== '') {
try {
$selectedRole = NamePolicy::normalizeRoleName($selectedRole, $prefix);
} catch (Throwable $e) {
$selectedRole = '';
}
}
if ($selectedRole === '' && !empty($dbUsers)) {
$selectedRole = $dbUsers[0];
}
$currentProfile = [];
if ($selectedRole !== '') {
$currentProfile = $ctx->mysql->getGrantProfile($selectedDb, $selectedRole);
if (empty($currentProfile) && in_array($selectedRole, $dbUsers, true)) {
$currentProfile = ['ALL'];
}
}
$dbOptions = '';
foreach ($dbNames as $dbName) {
$sel = ($dbName === $selectedDb) ? ' selected' : '';
$dbOptions .= '<option value="' . AppContext::e($dbName) . '"' . $sel . '>' . AppContext::e($dbName) . '</option>';
}
$assignableRoles = array_values(array_diff($roleNames, $dbUsers));
$assignOptions = '<option value="">-- wybierz nowego użytkownika --</option>';
foreach ($assignableRoles as $role) {
$assignOptions .= '<option value="' . AppContext::e($role) . '">' . AppContext::e($role) . '</option>';
}
$usersRows = '';
foreach ($dbUsers as $role) {
$profile = $ctx->mysql->getGrantProfile($selectedDb, $role);
$label = (empty($profile) || in_array('ALL', $profile, true))
? 'Pełny dostęp'
: implode(', ', $profile);
$usersRows .= '<tr>';
$usersRows .= '<td>' . AppContext::e($role) . '</td>';
$usersRows .= '<td><span class="badge">' . AppContext::e($label) . '</span></td>';
$usersRows .= '<td class="actions">';
$usersRows .= '<a class="btn" href="' . AppContext::e($ctx->url('change_password.html', ['role' => $role])) . '">Zarządzaj</a>';
$usersRows .= '<a class="btn" href="' . AppContext::e($ctx->url('modify_privileges.html', ['db' => $selectedDb, 'role' => $role])) . '#przywileje">Przywileje</a>';
$usersRows .= '<form method="post" style="display:inline-flex;gap:6px;margin:0">';
$usersRows .= $ctx->csrfField('modify_privileges_submit');
$usersRows .= '<input type="hidden" name="db_name" value="' . AppContext::e($selectedDb) . '">';
$usersRows .= '<input type="hidden" name="role_name" value="' . AppContext::e($role) . '">';
$usersRows .= '<input type="hidden" name="form_action" value="revoke_user">';
$usersRows .= '<button class="btn danger" type="submit">Odbierz dostęp</button>';
$usersRows .= '</form>';
$usersRows .= '</td>';
$usersRows .= '</tr>';
}
if ($usersRows === '') {
$usersRows = '<tr><td colspan="3" class="muted">Brak użytkowników z dostępem do bazy.</td></tr>';
}
$privCheckboxes = '';
foreach (NamePolicy::PRIVILEGE_LABELS as $code => $label) {
$checked = in_array($code, $currentProfile, true) ? ' checked' : '';
$privCheckboxes .= '<label class="inline"><input type="checkbox" name="privileges[]" value="' . AppContext::e($code) . '"' . $checked . '> ' . AppContext::e($label) . '</label>';
}
$body = '';
$body .= '<section class="panel">';
$body .= '<h3>Zarządzaj bazą danych</h3>';
$body .= '<form method="get" action="' . AppContext::e($ctx->url('modify_privileges.html')) . '" class="actions" style="justify-content:flex-end">';
$body .= '<label style="margin:0">Baza: <select name="db" style="width:auto;display:inline-block;margin-left:8px">' . $dbOptions . '</select></label>';
$body .= '<button class="btn" type="submit">Przełącz</button>';
$body .= '</form>';
$body .= '<p class="section-text">Szczegółowe informacje o bazie danych.</p>';
$body .= '<table><tbody>';
$body .= '<tr><td><strong>Nazwa bazy danych</strong><br>' . AppContext::e($dbInfo['name']) . '</td><td><strong>Domyślne kodowanie znaków</strong><br>' . AppContext::e($dbInfo['encoding']) . '</td><td><strong>Domyślne porównywanie znaków</strong><br>' . AppContext::e($dbInfo['collation']) . '</td></tr>';
$body .= '<tr><td><strong>Rozmiar</strong><br>' . AppContext::e($dbInfo['size']) . '</td><td><strong>Użytkownicy</strong><br>' . AppContext::e((string)count($dbUsers)) . '</td><td><strong>Liczba Tabel</strong><br>' . AppContext::e((string)$dbStats['tables']) . '</td></tr>';
$body .= '<tr><td><strong>Widoki</strong><br>' . AppContext::e((string)$dbStats['views']) . '</td><td><strong>Wyzwalacze</strong><br>' . AppContext::e((string)$dbStats['triggers']) . '</td><td><strong>Rutyny i procedury</strong><br>' . AppContext::e((string)$dbStats['routines']) . '</td></tr>';
$body .= '</tbody></table>';
$body .= '</section>';
$body .= '<section class="panel" style="margin-top:14px">';
$body .= '<h3>Dostęp użytkownika</h3>';
$body .= '<p class="section-text">Lista użytkowników, którzy mają dostęp do tej bazy danych wraz z podsumowaniem uprawnień.</p>';
$body .= '<table><thead><tr><th>Nazwa użytkownika</th><th>Przywileje</th><th>Akcje</th></tr></thead><tbody>' . $usersRows . '</tbody></table>';
$body .= '<form method="post" style="margin-top:10px">';
$body .= $ctx->csrfField('modify_privileges_submit');
$body .= '<input type="hidden" name="db_name" value="' . AppContext::e($selectedDb) . '">';
$body .= '<input type="hidden" name="form_action" value="grant_user">';
$body .= '<label>Przyznaj dostęp kolejnemu użytkownikowi</label>';
$body .= '<select name="role_name">' . $assignOptions . '</select>';
$body .= '<input type="hidden" name="grant_privileges[]" value="ALL">';
$body .= '<div class="actions"><button class="btn" type="submit">+ Przyznaj pełny dostęp</button></div>';
$body .= '</form>';
$body .= '</section>';
if ($selectedRole !== '') {
$body .= '<section class="panel" id="przywileje" style="margin-top:14px">';
$body .= '<h3>Przywileje użytkownika: ' . AppContext::e($selectedRole) . '</h3>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('modify_privileges_submit');
$body .= '<input type="hidden" name="db_name" value="' . AppContext::e($selectedDb) . '">';
$body .= '<input type="hidden" name="role_name" value="' . AppContext::e($selectedRole) . '">';
$body .= '<input type="hidden" name="form_action" value="save_privileges">';
$body .= '<div class="privileges-group" data-privileges-group>' . $privCheckboxes . '</div>';
$body .= '<div class="actions"><button class="primary" type="submit">Zapisz zmiany</button></div>';
$body .= '</form>';
$body .= '</section>';
}
$ctx->renderPage('Zarządzaj bazą danych', $body, $ok, $errors);
};
+135
View File
@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$rawMode = defined('DA_MYSQL_RAW_MODE') && DA_MYSQL_RAW_MODE === true;
$ensurePhpMyAdminReady = static function (): void {
$healthScript = PLUGIN_ROOT . '/scripts/setup/phpmyadmin_health_check.sh';
$repairScript = PLUGIN_ROOT . '/scripts/setup/phpmyadmin_repair.sh';
$runScript = static function (string $script, string &$output): int {
if (!is_file($script)) {
$output = 'Brak skryptu: ' . $script;
return 127;
}
$lines = [];
$code = 0;
exec('/bin/bash ' . escapeshellarg($script) . ' 2>&1', $lines, $code);
$output = implode("\n", $lines);
return $code;
};
$healthOutput = '';
if ($runScript($healthScript, $healthOutput) === 0) {
return;
}
$repairOutput = '';
$repairCode = $runScript($repairScript, $repairOutput);
if ($repairCode !== 0) {
throw new RuntimeException(
"Nie udało się naprawić konfiguracji phpMyAdmin.\n" . trim($repairOutput)
);
}
$healthAfterRepair = '';
if ($runScript($healthScript, $healthAfterRepair) !== 0) {
throw new RuntimeException(
"Konfiguracja phpMyAdmin nadal nie przechodzi health-check po repair.\n" . trim($healthAfterRepair)
);
}
};
if (!Http::isPost()) {
$message = 'Nieprawidłowa metoda żądania.';
if ($rawMode) {
echo "HTTP/1.1 405 Method Not Allowed\r\n";
echo "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
echo $message;
} else {
http_response_code(405);
header('Content-Type: text/plain; charset=UTF-8');
echo $message;
}
exit;
}
try {
$ensurePhpMyAdminReady();
} catch (Throwable $e) {
$message = 'Nie można przygotować phpMyAdmin: ' . $e->getMessage();
if ($rawMode) {
echo "HTTP/1.1 500 Internal Server Error\r\n";
echo "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
echo $message;
} else {
http_response_code(500);
header('Content-Type: text/plain; charset=UTF-8');
echo $message;
}
exit;
}
if (!$ctx->settings->enableAdminer()) {
$message = 'Integracja phpMyAdmin jest wyłączona na tym serwerze.';
if ($rawMode) {
echo "HTTP/1.1 403 Forbidden\r\n";
echo "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
echo $message;
} else {
http_response_code(403);
header('Content-Type: text/plain; charset=UTF-8');
echo $message;
}
exit;
}
try {
$ctx->requireCsrf('open_phpmyadmin_submit');
$requestedDb = trim($ctx->postString('adminer_db'));
if ($requestedDb === '') {
$requestedDb = null;
}
$sso = new PhpMyAdminSso($ctx->settings, $ctx->daUser, $ctx->mysql);
$payload = $sso->issueLoginPayload($requestedDb);
$target = (string)($payload['target'] ?? '');
if ($target === '') {
throw new RuntimeException('Nie udało się przygotować danych sesji do phpMyAdmin.');
}
if ($rawMode) {
echo "HTTP/1.1 302 Found\r\n";
echo "Location: " . $target . "\r\n";
echo "Cache-Control: no-store, no-cache, must-revalidate\r\n";
echo "Pragma: no-cache\r\n\r\n";
} else {
header('Location: ' . $target, true, 302);
}
exit;
} catch (Throwable $e) {
$message = 'Nie można otworzyć phpMyAdmin: ' . $e->getMessage();
@error_log(
sprintf(
"[%s] PHPMYADMIN_SSO_FAIL user=%s remote=%s message=%s\n",
date('c'),
$ctx->daUser->username(),
Http::server('REMOTE_ADDR'),
$e->getMessage()
),
3,
PLUGIN_ROOT . '/error.log'
);
if ($rawMode) {
echo "HTTP/1.1 400 Bad Request\r\n";
echo "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
echo $message;
} else {
http_response_code(400);
header('Content-Type: text/plain; charset=UTF-8');
echo $message;
}
exit;
}
};
File diff suppressed because it is too large Load Diff