Import alt-mysql plugin
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!defined('IN_DA_PLUGIN') || IN_DA_PLUGIN !== true) {
|
||||
http_response_code(403);
|
||||
echo 'Forbidden';
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!defined('PLUGIN_ACTION')) {
|
||||
http_response_code(400);
|
||||
echo 'Missing PLUGIN_ACTION';
|
||||
exit;
|
||||
}
|
||||
|
||||
define('PLUGIN_ROOT', dirname(__DIR__));
|
||||
define('PLUGIN_EXEC_DIR', __DIR__);
|
||||
define('PLUGIN_HANDLERS_DIR', __DIR__ . '/handlers');
|
||||
|
||||
$pluginErrorLog = PLUGIN_ROOT . '/error.log';
|
||||
@ini_set('log_errors', '1');
|
||||
@ini_set('error_log', $pluginErrorLog);
|
||||
if (!is_file($pluginErrorLog)) {
|
||||
@file_put_contents($pluginErrorLog, '');
|
||||
}
|
||||
|
||||
register_shutdown_function(static function () use ($pluginErrorLog): void {
|
||||
$last = error_get_last();
|
||||
if ($last === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
|
||||
if (!in_array((int)$last['type'], $fatalTypes, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$msg = sprintf(
|
||||
"[%s] FATAL type=%d message=%s file=%s line=%d\n",
|
||||
date('c'),
|
||||
(int)$last['type'],
|
||||
(string)$last['message'],
|
||||
(string)$last['file'],
|
||||
(int)$last['line']
|
||||
);
|
||||
@error_log($msg, 3, $pluginErrorLog);
|
||||
});
|
||||
|
||||
try {
|
||||
autoload_plugin_libs();
|
||||
Http::bootstrapGlobals();
|
||||
|
||||
$settings = Settings::load(PLUGIN_ROOT . '/plugin-settings.conf');
|
||||
PhpMyAdminRuntimeConfig::sync($settings, $pluginErrorLog);
|
||||
$daUser = DirectAdminUser::fromEnvironment($settings);
|
||||
|
||||
if (!$daUser->hasPluginAccess()) {
|
||||
http_response_code(403);
|
||||
$langCode = $daUser->language();
|
||||
$lang = Lang::load($daUser->language());
|
||||
render_access_denied_message($lang, $daUser->skin(), $langCode);
|
||||
exit;
|
||||
}
|
||||
|
||||
$credentials = Settings::loadMysqlCredentials($settings->mysqlCredentialsFile());
|
||||
$mysql = new MySQLService($credentials, $settings);
|
||||
$mysql->ensureMetadataSchema();
|
||||
|
||||
$secret = $settings->loadOrCreateSecret(PLUGIN_ROOT . '/data/secret.key');
|
||||
$csrf = new CsrfGuard($secret, $daUser->username(), Http::server('SESSION_ID'));
|
||||
|
||||
$lang = Lang::load($daUser->language());
|
||||
$ctx = new AppContext($daUser, $settings, $mysql, $csrf, $lang);
|
||||
|
||||
$handlerFile = PLUGIN_HANDLERS_DIR . '/' . basename((string)PLUGIN_ACTION) . '.php';
|
||||
if (!is_file($handlerFile)) {
|
||||
throw new RuntimeException('Brak handlera dla akcji: ' . (string)PLUGIN_ACTION);
|
||||
}
|
||||
|
||||
$handler = require $handlerFile;
|
||||
if (!is_callable($handler)) {
|
||||
throw new RuntimeException('Handler akcji nie jest wywoływalny: ' . (string)PLUGIN_ACTION);
|
||||
}
|
||||
|
||||
$handler($ctx);
|
||||
exit;
|
||||
} catch (Throwable $e) {
|
||||
$msg = sprintf(
|
||||
"[%s] EXCEPTION class=%s message=%s file=%s line=%d action=%s\n",
|
||||
date('c'),
|
||||
get_class($e),
|
||||
$e->getMessage(),
|
||||
$e->getFile(),
|
||||
$e->getLine(),
|
||||
defined('PLUGIN_ACTION') ? (string)PLUGIN_ACTION : 'unknown'
|
||||
);
|
||||
@error_log($msg, 3, $pluginErrorLog);
|
||||
@error_log("[stacktrace]\n" . $e->getTraceAsString() . "\n", 3, $pluginErrorLog);
|
||||
|
||||
http_response_code(500);
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
echo '<!doctype html><html><head><meta charset="utf-8"><title>Błąd pluginu</title></head><body>';
|
||||
echo '<h1>Błąd pluginu MySQL</h1>';
|
||||
echo '<p>' . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</p>';
|
||||
echo '<p>Sprawdź konfigurację i uprawnienia pluginu.</p>';
|
||||
echo '</body></html>';
|
||||
exit;
|
||||
}
|
||||
|
||||
function autoload_plugin_libs(): void
|
||||
{
|
||||
$files = [
|
||||
PLUGIN_EXEC_DIR . '/lib/Http.php',
|
||||
PLUGIN_EXEC_DIR . '/lib/Settings.php',
|
||||
PLUGIN_EXEC_DIR . '/lib/PhpMyAdminRuntimeConfig.php',
|
||||
PLUGIN_EXEC_DIR . '/lib/DirectAdminUser.php',
|
||||
PLUGIN_EXEC_DIR . '/lib/Lang.php',
|
||||
PLUGIN_EXEC_DIR . '/lib/LocalizedException.php',
|
||||
PLUGIN_EXEC_DIR . '/lib/CsrfGuard.php',
|
||||
PLUGIN_EXEC_DIR . '/lib/NamePolicy.php',
|
||||
PLUGIN_EXEC_DIR . '/lib/MySQLService.php',
|
||||
PLUGIN_EXEC_DIR . '/lib/PhpMyAdminSso.php',
|
||||
PLUGIN_EXEC_DIR . '/lib/BackupQueueService.php',
|
||||
PLUGIN_EXEC_DIR . '/lib/FilePicker.php',
|
||||
PLUGIN_EXEC_DIR . '/lib/AppContext.php',
|
||||
];
|
||||
|
||||
foreach ($files as $file) {
|
||||
require_once $file;
|
||||
}
|
||||
}
|
||||
|
||||
function render_access_denied_message(Lang $lang, string $skin, string $langCode): void
|
||||
{
|
||||
$path = PLUGIN_ROOT . '/user/' . strtolower(trim($skin)) . '.css';
|
||||
if (!is_file($path)) {
|
||||
$path = PLUGIN_ROOT . '/user/enhanced.css';
|
||||
}
|
||||
$css = is_file($path) ? (string)file_get_contents($path) : '';
|
||||
|
||||
$message = $lang->t('Bazy danych MySQL nie są dostępne dla Twojego konta - skontaktuj się z pomocą techniczną aby uzyskać pomoc i więcej informacji');
|
||||
$title = $lang->t('Błąd');
|
||||
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
echo '<!doctype html><html lang="' . htmlspecialchars($langCode, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '"><head><meta charset="utf-8">';
|
||||
echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
|
||||
echo '<title>' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</title>';
|
||||
echo '<style>' . $css . '</style>';
|
||||
echo '</head><body>';
|
||||
echo '<div class="wrap">';
|
||||
echo '<section class="main-section">';
|
||||
echo '<div class="alert alert-err">' . htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>';
|
||||
echo '</section>';
|
||||
echo '</div>';
|
||||
echo '</body></html>';
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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'));
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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';
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
@@ -0,0 +1,333 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class AppContext
|
||||
{
|
||||
public DirectAdminUser $daUser;
|
||||
public Settings $settings;
|
||||
public MySQLService $mysql;
|
||||
|
||||
private CsrfGuard $csrf;
|
||||
private Lang $lang;
|
||||
|
||||
public function __construct(DirectAdminUser $daUser, Settings $settings, MySQLService $mysql, CsrfGuard $csrf, Lang $lang)
|
||||
{
|
||||
$this->daUser = $daUser;
|
||||
$this->settings = $settings;
|
||||
$this->mysql = $mysql;
|
||||
$this->csrf = $csrf;
|
||||
$this->lang = $lang;
|
||||
}
|
||||
|
||||
public function action(): string
|
||||
{
|
||||
return (string)PLUGIN_ACTION;
|
||||
}
|
||||
|
||||
public function baseUrl(): string
|
||||
{
|
||||
return '/CMD_PLUGINS/alt-mysql';
|
||||
}
|
||||
|
||||
public function url(string $page, array $query = []): string
|
||||
{
|
||||
$path = $this->baseUrl() . '/' . ltrim($page, '/');
|
||||
if (!empty($query)) {
|
||||
$path .= '?' . http_build_query($query);
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function isPost(): bool
|
||||
{
|
||||
return Http::isPost();
|
||||
}
|
||||
|
||||
public function postString(string $key, string $default = ''): string
|
||||
{
|
||||
return Http::getString($_POST, $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,string>
|
||||
*/
|
||||
public function postArray(string $key): array
|
||||
{
|
||||
$raw = Http::getArray($_POST, $key);
|
||||
$out = [];
|
||||
foreach ($raw as $item) {
|
||||
if (is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$out[] = trim((string)$item);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function queryString(string $key, string $default = ''): string
|
||||
{
|
||||
return Http::getString($_GET, $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string|int|float> $vars
|
||||
*/
|
||||
public function t(string $key, array $vars = []): string
|
||||
{
|
||||
return $this->lang->t($key, $vars);
|
||||
}
|
||||
|
||||
public function formatException(Throwable $e): string
|
||||
{
|
||||
if ($e instanceof LocalizedException) {
|
||||
return $this->t($e->key(), $e->vars());
|
||||
}
|
||||
return $this->t($e->getMessage());
|
||||
}
|
||||
|
||||
public function csrfField(string $intent): string
|
||||
{
|
||||
$issued = $this->csrf->issue($intent);
|
||||
$ts = (string)$issued['ts'];
|
||||
$sid = htmlspecialchars((string)($issued['sid'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$token = htmlspecialchars($issued['token'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
|
||||
return '<input type="hidden" name="csrf_ts" value="' . $ts . '">' .
|
||||
'<input type="hidden" name="csrf_sid" value="' . $sid . '">' .
|
||||
'<input type="hidden" name="csrf_token" value="' . $token . '">';
|
||||
}
|
||||
|
||||
public function requireCsrf(string $intent): void
|
||||
{
|
||||
$ts = $this->postString('csrf_ts');
|
||||
$sid = $this->postString('csrf_sid');
|
||||
$token = $this->postString('csrf_token');
|
||||
if (!$this->csrf->validate($intent, $ts, $token, 3600, $sid)) {
|
||||
$debug = sprintf(
|
||||
'[%s] CSRF_FAIL intent=%s action=%s method=%s ts=%s sid=%s token_prefix=%s post_keys=%s',
|
||||
date('c'),
|
||||
$intent,
|
||||
$this->action(),
|
||||
Http::method(),
|
||||
$ts,
|
||||
$sid,
|
||||
substr($token, 0, 12),
|
||||
implode(',', array_keys($_POST))
|
||||
);
|
||||
@error_log($debug . "\n", 3, PLUGIN_ROOT . '/error.log');
|
||||
throw new RuntimeException('Nieprawidłowy lub wygasły token CSRF.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $okMessages
|
||||
* @param string[] $errorMessages
|
||||
*/
|
||||
public function renderPage(string $title, string $body, array $okMessages = [], array $errorMessages = [], string $logs = ''): void
|
||||
{
|
||||
$alerts = '';
|
||||
foreach ($okMessages as $msg) {
|
||||
$alerts .= '<div class="alert alert-ok">' . self::e($msg) . '</div>';
|
||||
}
|
||||
foreach ($errorMessages as $msg) {
|
||||
$alerts .= '<div class="alert alert-err">' . self::e($msg) . '</div>';
|
||||
}
|
||||
|
||||
$alerts = $this->lang->translateHtml($alerts);
|
||||
$body = $this->lang->translateHtml($body);
|
||||
$logs = $this->lang->translateHtml($logs);
|
||||
|
||||
$safeTitle = self::e($this->t($title));
|
||||
$topActions = $this->renderTopActions();
|
||||
$langCode = $this->daUser->language();
|
||||
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
echo '<!doctype html><html lang="' . self::e($langCode) . '"><head><meta charset="utf-8">';
|
||||
echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
|
||||
echo '<title>' . $safeTitle . '</title>';
|
||||
echo '<style>' . $this->styles() . '</style>';
|
||||
echo '</head><body>';
|
||||
echo '<div class="wrap">';
|
||||
echo '<div class="breadcrumbs">' . self::e($this->t('Pulpit')) . ' <span>></span> ' . self::e($this->t('Bazy danych')) . '</div>';
|
||||
$headerClass = in_array($this->action(), ['index', 'upload_backup'], true) ? ' class="header-center"' : '';
|
||||
echo '<header' . $headerClass . '><h1>' . $safeTitle . '</h1></header>';
|
||||
echo '<div class="top-actions">' . $topActions . '</div>';
|
||||
echo '<section class="main-section">' . $alerts . $body . '</section>';
|
||||
echo '<section class="logs-section">' . $logs . '</section>';
|
||||
echo '</div>';
|
||||
$jsConfig = 'window.DA_MYSQL_BASE_URL=' . json_encode($this->baseUrl(), JSON_UNESCAPED_SLASHES) . ';';
|
||||
$jsConfig .= 'window.DA_MYSQL_INDEX_URL=' . json_encode($this->url('index.html'), JSON_UNESCAPED_SLASHES) . ';';
|
||||
$jsConfig .= 'window.DA_MYSQL_USER_URL=' . json_encode($this->url('user/index.html'), JSON_UNESCAPED_SLASHES) . ';';
|
||||
$jsConfig .= 'window.DA_MYSQL_UPLOAD_RAW_URL=' . json_encode($this->url('upload_backup.raw'), JSON_UNESCAPED_SLASHES) . ';';
|
||||
$jsConfig .= 'window.DA_MYSQL_I18N=' . json_encode($this->jsI18n(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';';
|
||||
echo '<script>' . $jsConfig . '</script>';
|
||||
echo '<script>' . $this->scripts() . '</script>';
|
||||
echo '</body></html>';
|
||||
}
|
||||
|
||||
public function renderFatal(string $message, int $code = 400): void
|
||||
{
|
||||
http_response_code($code);
|
||||
$body = '<p>' . self::e($this->t($message)) . '</p>';
|
||||
$body .= '<p><a href="' . self::e($this->url('index.html')) . '">' . self::e($this->t('Powrót')) . '</a></p>';
|
||||
$this->renderPage('Błąd', $body, [], []);
|
||||
}
|
||||
|
||||
public static function e(string $value): string
|
||||
{
|
||||
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
private function renderTopActions(): string
|
||||
{
|
||||
$links = [];
|
||||
|
||||
$links[] = '<a class="btn amber' . ($this->action() === 'index' ? ' active' : '') . '" href="' . self::e($this->url('index.html')) . '">' . self::e($this->t('BAZY DANYCH')) . '</a>';
|
||||
$links[] = '<a class="btn amber' . ($this->action() === 'change_password' ? ' active' : '') . '" href="' . self::e($this->url('change_password.html')) . '">' . self::e($this->t('UŻYTKOWNICY BAZ DANYCH')) . '</a>';
|
||||
if ($this->settings->enableUploadBackup()) {
|
||||
$links[] = '<a class="btn amber' . ($this->action() === 'upload_backup' ? ' active' : '') . '" href="' . self::e($this->url('upload_backup.html')) . '">' . self::e($this->t('BACKUPY BAZ DANYCH')) . '</a>';
|
||||
}
|
||||
if ($this->settings->enableAdminer()) {
|
||||
$links[] = '<form method="post" target="_top" action="' . self::e($this->url('open_phpmyadmin.raw')) . '" style="display:inline-flex;align-items:center;">' .
|
||||
$this->csrfField('open_phpmyadmin_submit') .
|
||||
'<button class="btn amber' . ($this->action() === 'open_phpmyadmin' ? ' active' : '') . '" type="submit">' . self::e($this->t('PHPMYADMIN')) . '</button>' .
|
||||
'</form>';
|
||||
}
|
||||
|
||||
return implode('', $links);
|
||||
}
|
||||
|
||||
private function styles(): string
|
||||
{
|
||||
$skin = $this->daUser->skin();
|
||||
$path = PLUGIN_ROOT . '/user/' . $skin . '.css';
|
||||
if (!is_file($path)) {
|
||||
$path = PLUGIN_ROOT . '/user/enhanced.css';
|
||||
}
|
||||
$css = is_file($path) ? file_get_contents($path) : '';
|
||||
if ($css === false) {
|
||||
return '';
|
||||
}
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
*/
|
||||
private function jsI18n(): array
|
||||
{
|
||||
$keys = [
|
||||
'Pobieranie...',
|
||||
'Nieudana odpowiedź serwera ({code})',
|
||||
'Nieudana odpowiedź serwera (HTML zamiast pliku, URL: {url})',
|
||||
'Serwer zwrócił pusty plik.',
|
||||
'Nie można pobrać pliku backupu.',
|
||||
'Pobierz plik backupu',
|
||||
'Brak plików .sql/.gz',
|
||||
'Brak katalogów',
|
||||
'Nie udało się wczytać katalogów.',
|
||||
'Podaj nazwę katalogu.',
|
||||
'Nie udało się utworzyć katalogu.',
|
||||
'Wysyłanie pliku...',
|
||||
'Nie udało się wysłać pliku backupu.',
|
||||
'Wybierz plik .sql/.gz/.sql.gz lub wskaż ścieżkę pliku na serwerze.',
|
||||
'Wskaż ścieżkę do pliku SQL lub SQL.GZ.',
|
||||
'Wybierz plik .sql/.gz/.sql.gz do przywrócenia.',
|
||||
];
|
||||
|
||||
$out = [];
|
||||
foreach ($keys as $key) {
|
||||
$out[$key] = $this->t($key);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function scripts(): string
|
||||
{
|
||||
$js = <<<'JS'
|
||||
(function () {
|
||||
var __i18n = (typeof window.DA_MYSQL_I18N === 'object' && window.DA_MYSQL_I18N) ? window.DA_MYSQL_I18N : {};
|
||||
function tr(key) {
|
||||
return (__i18n && Object.prototype.hasOwnProperty.call(__i18n, key)) ? __i18n[key] : key;
|
||||
}
|
||||
function trf(key, vars) {
|
||||
var out = tr(key);
|
||||
if (!vars) {
|
||||
return out;
|
||||
}
|
||||
for (var k in vars) {
|
||||
if (!Object.prototype.hasOwnProperty.call(vars, k)) {
|
||||
continue;
|
||||
}
|
||||
out = out.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function generatePassword(len) {
|
||||
var size = len || 20;
|
||||
var alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*_-+=';
|
||||
var bytes = new Uint32Array(size);
|
||||
if (window.crypto && window.crypto.getRandomValues) {
|
||||
window.crypto.getRandomValues(bytes);
|
||||
} else {
|
||||
for (var i = 0; i < size; i += 1) {
|
||||
bytes[i] = Math.floor(Math.random() * 0xffffffff);
|
||||
}
|
||||
}
|
||||
var out = '';
|
||||
for (var j = 0; j < size; j += 1) {
|
||||
out += alphabet.charAt(bytes[j] % alphabet.length);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function bindPasswordHelpers() {
|
||||
document.querySelectorAll('[data-generate-target]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var input = document.getElementById(btn.getAttribute('data-generate-target'));
|
||||
if (!input) { return; }
|
||||
input.value = generatePassword(20);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-toggle-target]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var input = document.getElementById(btn.getAttribute('data-toggle-target'));
|
||||
if (!input) { return; }
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindDownloadForms() {
|
||||
document.querySelectorAll('form[data-backup-download]').forEach(function (form) {
|
||||
form.addEventListener('submit', function (ev) {
|
||||
var btn = form.querySelector('button[type="submit"],input[type="submit"]');
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.dataset.originalText = btn.textContent;
|
||||
btn.textContent = tr('Pobieranie...');
|
||||
}
|
||||
window.setTimeout(function () {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
if (btn.dataset.originalText) {
|
||||
btn.textContent = btn.dataset.originalText;
|
||||
}
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.daMysqlI18n = { tr: tr, trf: trf };
|
||||
bindPasswordHelpers();
|
||||
bindDownloadForms();
|
||||
}());
|
||||
JS;
|
||||
|
||||
return FilePicker::scripts() . "\n" . $js;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class CsrfGuard
|
||||
{
|
||||
private string $secret;
|
||||
private string $username;
|
||||
private string $sessionId;
|
||||
|
||||
public function __construct(string $secret, string $username, string $sessionId)
|
||||
{
|
||||
$this->secret = $secret;
|
||||
$this->username = $username;
|
||||
$this->sessionId = $sessionId !== '' ? $sessionId : 'no_session';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ts:int,token:string}
|
||||
*/
|
||||
public function issue(string $intent): array
|
||||
{
|
||||
$ts = time();
|
||||
$sid = $this->sessionId;
|
||||
return [
|
||||
'ts' => $ts,
|
||||
'sid' => $sid,
|
||||
'token' => $this->buildToken($intent, $ts, $sid),
|
||||
];
|
||||
}
|
||||
|
||||
public function validate(string $intent, string $timestamp, string $token, int $ttl = 3600, string $sessionHint = ''): bool
|
||||
{
|
||||
if (!preg_match('/^[0-9]{1,20}$/', $timestamp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ts = (int)$timestamp;
|
||||
$now = time();
|
||||
if ($ts <= 0 || $ts > ($now + 30) || ($now - $ts) > $ttl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = trim($token);
|
||||
$sids = [];
|
||||
|
||||
$hint = trim($sessionHint);
|
||||
if ($hint !== '') {
|
||||
$sids[] = $hint;
|
||||
}
|
||||
$sids[] = $this->sessionId;
|
||||
$sids[] = 'no_session';
|
||||
$sids[] = '';
|
||||
$sids = array_values(array_unique($sids));
|
||||
|
||||
foreach ($sids as $sid) {
|
||||
$expected = $this->buildToken($intent, $ts, $sid);
|
||||
if (hash_equals($expected, $token)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function buildToken(string $intent, int $timestamp, string $sid): string
|
||||
{
|
||||
$payload = implode('|', [$this->username, $sid !== '' ? $sid : 'no_session', $intent, (string)$timestamp]);
|
||||
return hash_hmac('sha256', $payload, $this->secret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class DirectAdminUser
|
||||
{
|
||||
private string $username;
|
||||
private string $prefix;
|
||||
|
||||
/** @var array<string,string> */
|
||||
private array $userConf;
|
||||
|
||||
/** @var array<string,string> */
|
||||
private array $packageConf;
|
||||
|
||||
private Settings $settings;
|
||||
|
||||
private function __construct(string $username, array $userConf, array $packageConf, Settings $settings)
|
||||
{
|
||||
$this->username = $username;
|
||||
$this->prefix = $username . '_';
|
||||
$this->userConf = $userConf;
|
||||
$this->packageConf = $packageConf;
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
public static function fromEnvironment(Settings $settings): self
|
||||
{
|
||||
$username = Http::server('USER');
|
||||
if ($username === '') {
|
||||
$username = Http::server('USERNAME');
|
||||
}
|
||||
|
||||
if ($username === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $username)) {
|
||||
throw new RuntimeException('Nie można ustalić użytkownika DirectAdmin.');
|
||||
}
|
||||
|
||||
$userConfPath = '/usr/local/directadmin/data/users/' . $username . '/user.conf';
|
||||
$userConf = self::loadSimpleConf($userConfPath);
|
||||
$packageConf = self::loadPackageConf($username, $userConf);
|
||||
|
||||
return new self($username, $userConf, $packageConf, $settings);
|
||||
}
|
||||
|
||||
public function username(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function prefix(): string
|
||||
{
|
||||
return $this->prefix;
|
||||
}
|
||||
|
||||
public function language(): string
|
||||
{
|
||||
$lang = strtolower(trim((string)($this->userConf['language'] ?? $this->userConf['lang'] ?? 'en')));
|
||||
if ($lang === '' || strpos($lang, 'en') === 0) {
|
||||
return 'en';
|
||||
}
|
||||
if (strpos($lang, 'pl') === 0) {
|
||||
return 'pl';
|
||||
}
|
||||
return 'en';
|
||||
}
|
||||
|
||||
public function skin(): string
|
||||
{
|
||||
$skin = strtolower(trim((string)($this->userConf['skin'] ?? 'enhanced')));
|
||||
if (strpos($skin, 'evolution') !== false) {
|
||||
return 'evolution';
|
||||
}
|
||||
if (strpos($skin, 'enhanced') !== false) {
|
||||
return 'enhanced';
|
||||
}
|
||||
return 'enhanced';
|
||||
}
|
||||
|
||||
public function hasPluginAccess(): bool
|
||||
{
|
||||
if ($this->settings->alwaysEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->isPluginEnabledByCustomItem() && $this->isDatabaseQuotaEnabled() && $this->isPluginAllowedByPluginRules();
|
||||
}
|
||||
|
||||
public function pluginAccessErrorMessage(): string
|
||||
{
|
||||
if (!$this->isPluginEnabledByCustomItem()) {
|
||||
return 'Plugin MySQL jest wyłączony dla tego konta lub pakietu.';
|
||||
}
|
||||
|
||||
if (!$this->isDatabaseQuotaEnabled()) {
|
||||
return 'Plugin MySQL jest wyłączony: limit baz danych MySQL ustawiono na 0 i nie zaznaczono opcji Bez Ograniczeń.';
|
||||
}
|
||||
|
||||
if (!$this->isPluginAllowedByPluginRules()) {
|
||||
return 'Plugin MySQL jest zablokowany przez reguły plugins_allow/plugins_deny.';
|
||||
}
|
||||
|
||||
return 'Brak dostępu do pluginu MySQL.';
|
||||
}
|
||||
|
||||
public function maxDatabases(): int
|
||||
{
|
||||
if ($this->settings->alwaysEnabled()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($this->isUnlimitedDatabasesByCustomItem()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$rawValue = $this->rawDatabasesLimitValue();
|
||||
if ($rawValue === null || trim($rawValue) === '') {
|
||||
$default = $this->settings->defaultDatabasesLimit();
|
||||
return $default < 0 ? 0 : $default;
|
||||
}
|
||||
|
||||
$normalized = strtolower(trim($rawValue));
|
||||
if ($normalized === 'unlimited') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!preg_match('/^[0-9]+$/', $normalized)) {
|
||||
$default = $this->settings->defaultDatabasesLimit();
|
||||
return $default < 0 ? 0 : $default;
|
||||
}
|
||||
|
||||
$limit = (int)$normalized;
|
||||
return $limit < 0 ? 0 : $limit;
|
||||
}
|
||||
|
||||
private function isPluginEnabledByCustomItem(): bool
|
||||
{
|
||||
$raw = $this->readScopedValue('mysql_enabled');
|
||||
if ($raw === null) {
|
||||
$raw = $this->readScopedValue('da_mysql_enabled');
|
||||
}
|
||||
if ($raw === null) {
|
||||
return false;
|
||||
}
|
||||
return Settings::toBool($raw, false);
|
||||
}
|
||||
|
||||
private function isUnlimitedDatabasesByCustomItem(): bool
|
||||
{
|
||||
$unlimitedFlags = [
|
||||
$this->readScopedValue('umysql'),
|
||||
$this->readScopedValue('umysql_max_databases'),
|
||||
$this->readScopedValue('mysql_unlimited'),
|
||||
];
|
||||
|
||||
foreach ($unlimitedFlags as $raw) {
|
||||
if ($raw !== null && Settings::toBool($raw, false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$rawLimit = $this->rawDatabasesLimitValue();
|
||||
return $rawLimit !== null && strtolower(trim($rawLimit)) === 'unlimited';
|
||||
}
|
||||
|
||||
private function isDatabaseQuotaEnabled(): bool
|
||||
{
|
||||
if ($this->isUnlimitedDatabasesByCustomItem()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->maxDatabases() > 0;
|
||||
}
|
||||
|
||||
private function rawDatabasesLimitValue(): ?string
|
||||
{
|
||||
$raw = $this->readScopedValue('mysql_max_databases');
|
||||
if ($raw === null || trim($raw) === '') {
|
||||
$raw = $this->readScopedValue('mysql');
|
||||
}
|
||||
return $raw;
|
||||
}
|
||||
|
||||
private function isPluginAllowedByPluginRules(): bool
|
||||
{
|
||||
$pluginId = 'alt-mysql';
|
||||
|
||||
foreach ([$this->packageConf, $this->userConf] as $conf) {
|
||||
if (!empty($conf['plugins_allow'])) {
|
||||
$allow = self::parsePluginsList($conf['plugins_allow']);
|
||||
return in_array($pluginId, $allow, true);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ([$this->packageConf, $this->userConf] as $conf) {
|
||||
if (!empty($conf['plugins_deny'])) {
|
||||
$deny = self::parsePluginsList($conf['plugins_deny']);
|
||||
return !in_array($pluginId, $deny, true);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function readScopedValue(string $key): ?string
|
||||
{
|
||||
if (array_key_exists($key, $this->userConf)) {
|
||||
return $this->userConf[$key];
|
||||
}
|
||||
if (array_key_exists($key, $this->packageConf)) {
|
||||
return $this->packageConf[$key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
*/
|
||||
private static function loadSimpleConf(string $path): array
|
||||
{
|
||||
$data = [];
|
||||
if (!is_readable($path)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES);
|
||||
if ($lines === false) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!preg_match('/^([A-Za-z0-9_]+)=(.*)$/', $line, $m)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data[strtolower($m[1])] = trim((string)$m[2]);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $userConf
|
||||
* @return array<string,string>
|
||||
*/
|
||||
private static function loadPackageConf(string $username, array $userConf): array
|
||||
{
|
||||
$package = trim((string)($userConf['package'] ?? $userConf['user_package'] ?? ''));
|
||||
if ($package === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $package)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$candidates = [];
|
||||
$owners = [];
|
||||
foreach (['creator', 'owner', 'reseller', 'username'] as $key) {
|
||||
if (!empty($userConf[$key]) && preg_match('/^[A-Za-z0-9._-]+$/', $userConf[$key])) {
|
||||
$owners[] = $userConf[$key];
|
||||
}
|
||||
}
|
||||
$owners[] = $username;
|
||||
$owners[] = 'admin';
|
||||
$owners = array_values(array_unique($owners));
|
||||
|
||||
foreach ($owners as $owner) {
|
||||
$candidates[] = '/usr/local/directadmin/data/users/' . $owner . '/packages/' . $package;
|
||||
$candidates[] = '/usr/local/directadmin/data/users/' . $owner . '/packages/' . $package . '.conf';
|
||||
}
|
||||
|
||||
foreach ($candidates as $path) {
|
||||
$conf = self::loadSimpleConf($path);
|
||||
if (!empty($conf)) {
|
||||
return $conf;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private static function parsePluginsList(string $value): array
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach (explode(':', strtolower($value)) as $item) {
|
||||
$item = trim($item);
|
||||
if ($item !== '') {
|
||||
$items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($items));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class FilePicker
|
||||
{
|
||||
public static function styles(): string
|
||||
{
|
||||
return <<<'CSS'
|
||||
.br-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
.br-modal {
|
||||
width: min(560px, 92vw);
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 2px solid #d8dde6;
|
||||
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.25);
|
||||
}
|
||||
.br-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.br-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.br-modal-title {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
.br-modal-body {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.br-muted { color: #64748b; }
|
||||
.br-alert {
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
margin: 8px 0;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.br-alert-error { background: #fff1f2; border-color: #fecdd3; color: #9f1239; }
|
||||
.br-picker-path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.br-picker-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid #d8dde6;
|
||||
border-radius: 10px;
|
||||
max-height: 45vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.br-picker-create {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.br-picker-create input {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #d8dde6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.br-picker-list li {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #d8dde6;
|
||||
}
|
||||
.br-picker-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.br-picker-list li:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.br-picker-list li.active {
|
||||
background: #e0f2fe;
|
||||
}
|
||||
.br-btn {
|
||||
background: #b77200;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.br-btn:hover { background: #9a6200; }
|
||||
.br-btn-ghost {
|
||||
background: transparent;
|
||||
color: #1f2d3d;
|
||||
border: 1px solid #d8dde6;
|
||||
}
|
||||
.br-btn-ghost:hover { background: #eef2f7; }
|
||||
.br-btn-small {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.br-icon-btn {
|
||||
border: 1px solid #d8dde6;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
.br-icon-btn:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
CSS;
|
||||
}
|
||||
|
||||
public static function scripts(): string
|
||||
{
|
||||
return <<<'JS'
|
||||
var pickerTarget = null;
|
||||
var pickerPath = '';
|
||||
var pickerBase = '';
|
||||
var pickerMode = 'dir';
|
||||
var pickerSelected = '';
|
||||
var pickerUrl = '';
|
||||
var pickerRoot = '';
|
||||
|
||||
function openPicker(inputId, mode) {
|
||||
var modal = document.getElementById('br-picker');
|
||||
if (!modal) { return; }
|
||||
var target = document.getElementById(inputId);
|
||||
if (!target) { return; }
|
||||
pickerTarget = target;
|
||||
pickerMode = mode === 'file' ? 'file' : 'dir';
|
||||
pickerSelected = '';
|
||||
pickerUrl = modal.getAttribute('data-picker-url') || window.location.href;
|
||||
pickerRoot = modal.getAttribute('data-picker-root') || '';
|
||||
|
||||
var initial = (target.value || '').trim();
|
||||
if (initial === '') {
|
||||
initial = pickerRoot || '';
|
||||
}
|
||||
var createRow = document.getElementById('br-picker-create');
|
||||
if (createRow) {
|
||||
createRow.style.display = (pickerMode === 'dir') ? 'flex' : 'none';
|
||||
}
|
||||
loadPicker(initial);
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function closePicker() {
|
||||
var modal = document.getElementById('br-picker');
|
||||
if (modal) { modal.style.display = 'none'; }
|
||||
}
|
||||
|
||||
function pickerSetError(msg) {
|
||||
var el = document.getElementById('br-picker-error');
|
||||
if (!el) { return; }
|
||||
if (msg) {
|
||||
el.textContent = msg;
|
||||
el.style.display = 'block';
|
||||
} else {
|
||||
el.textContent = '';
|
||||
el.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function renderPickerList(items) {
|
||||
var list = document.getElementById('br-picker-list');
|
||||
if (!list) { return; }
|
||||
list.innerHTML = '';
|
||||
if (!items || !items.length) {
|
||||
var empty = document.createElement('li');
|
||||
empty.textContent = pickerMode === 'file' ? tr('Brak plików .sql/.gz') : tr('Brak katalogów');
|
||||
empty.className = 'br-muted';
|
||||
list.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
items.forEach(function (entry) {
|
||||
var li = document.createElement('li');
|
||||
if (entry.type === 'dir') {
|
||||
li.textContent = '📁 ' + entry.name + '/';
|
||||
li.addEventListener('click', function () {
|
||||
pickerSelected = '';
|
||||
loadPicker(pickerPath.replace(/\/+$/, '') + '/' + entry.name);
|
||||
});
|
||||
} else {
|
||||
var full = pickerPath.replace(/\/+$/, '') + '/' + entry.name;
|
||||
li.textContent = '📄 ' + entry.name;
|
||||
li.addEventListener('click', function () {
|
||||
pickerSelected = full;
|
||||
var active = list.querySelectorAll('li.active');
|
||||
for (var i = 0; i < active.length; i += 1) {
|
||||
active[i].classList.remove('active');
|
||||
}
|
||||
li.classList.add('active');
|
||||
});
|
||||
if (pickerSelected === full) {
|
||||
li.classList.add('active');
|
||||
}
|
||||
}
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function extractPickerJson(text) {
|
||||
var marker = '__DA_MYSQL_PICKER_JSON__';
|
||||
var start = text.indexOf(marker);
|
||||
if (start === -1) { return null; }
|
||||
start += marker.length;
|
||||
var end = text.indexOf(marker, start);
|
||||
if (end === -1) { return null; }
|
||||
var jsonText = text.substring(start, end).trim();
|
||||
try { return JSON.parse(jsonText); } catch (e) { return null; }
|
||||
}
|
||||
|
||||
function loadPicker(path) {
|
||||
pickerSetError('');
|
||||
var url = pickerUrl + '?action=dir_list&mode=' + encodeURIComponent(pickerMode) + '&path=' + encodeURIComponent(path || '');
|
||||
fetch(url, { credentials: 'same-origin' })
|
||||
.then(function (r) { return r.text(); })
|
||||
.then(function (text) {
|
||||
var data = extractPickerJson(text || '');
|
||||
if (!data || !data.ok) {
|
||||
pickerSetError(tr('Nie udało się wczytać katalogów.'));
|
||||
return;
|
||||
}
|
||||
pickerPath = data.path || '';
|
||||
pickerBase = data.base || '';
|
||||
if (data.selected) {
|
||||
pickerSelected = data.selected;
|
||||
}
|
||||
var current = document.getElementById('br-picker-current');
|
||||
if (current) { current.textContent = pickerPath; }
|
||||
renderPickerList(data.items || []);
|
||||
})
|
||||
.catch(function () {
|
||||
pickerSetError(tr('Nie udało się wczytać katalogów.'));
|
||||
});
|
||||
}
|
||||
|
||||
function pickerGoUp() {
|
||||
if (!pickerPath) { return; }
|
||||
if (pickerBase && pickerPath === pickerBase) { return; }
|
||||
var up = pickerPath.replace(/\/+$/, '');
|
||||
up = up.substring(0, up.lastIndexOf('/')) || '/';
|
||||
loadPicker(up);
|
||||
}
|
||||
|
||||
function createPickerDir() {
|
||||
var input = document.getElementById('br-picker-new');
|
||||
if (!input) { return; }
|
||||
var name = (input.value || '').trim();
|
||||
if (name === '') {
|
||||
pickerSetError(tr('Podaj nazwę katalogu.'));
|
||||
return;
|
||||
}
|
||||
pickerSetError('');
|
||||
var url = pickerUrl + '?action=dir_create&path=' + encodeURIComponent(pickerPath || '') + '&name=' + encodeURIComponent(name);
|
||||
fetch(url, { credentials: 'same-origin' })
|
||||
.then(function (r) { return r.text(); })
|
||||
.then(function (text) {
|
||||
var data = extractPickerJson(text || '');
|
||||
if (!data || !data.ok) {
|
||||
pickerSetError(tr('Nie udało się utworzyć katalogu.'));
|
||||
return;
|
||||
}
|
||||
input.value = '';
|
||||
loadPicker(pickerPath);
|
||||
})
|
||||
.catch(function () {
|
||||
pickerSetError(tr('Nie udało się utworzyć katalogu.'));
|
||||
});
|
||||
}
|
||||
|
||||
function selectPickerPath() {
|
||||
if (!pickerTarget) { closePicker(); return; }
|
||||
if (pickerMode === 'file' && pickerSelected) {
|
||||
pickerTarget.value = pickerSelected || '';
|
||||
} else if (pickerMode === 'dir') {
|
||||
pickerTarget.value = pickerPath || '';
|
||||
}
|
||||
try {
|
||||
pickerTarget.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
pickerTarget.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
} catch (e) {
|
||||
// ignore dispatch errors on older browsers
|
||||
}
|
||||
if (typeof window.__restoreFileSync === 'function') {
|
||||
window.__restoreFileSync();
|
||||
}
|
||||
closePicker();
|
||||
}
|
||||
|
||||
function bindFilePicker() {
|
||||
window.openPicker = openPicker;
|
||||
window.closePicker = closePicker;
|
||||
window.pickerGoUp = pickerGoUp;
|
||||
window.createPickerDir = createPickerDir;
|
||||
window.selectPickerPath = selectPickerPath;
|
||||
}
|
||||
JS;
|
||||
}
|
||||
|
||||
public static function renderOverlay(AppContext $ctx, string $overlayId, string $rootPath, string $listUrl, string $inputId = 'source-file-path'): string
|
||||
{
|
||||
$safeRoot = AppContext::e($rootPath);
|
||||
$safeUrl = AppContext::e($listUrl);
|
||||
|
||||
$title = $ctx->t('Wybierz plik');
|
||||
$close = $ctx->t('Zamknij');
|
||||
$up = $ctx->t('W górę');
|
||||
$newDir = $ctx->t('Nowy katalog');
|
||||
$create = $ctx->t('Utwórz');
|
||||
$cancel = $ctx->t('Anuluj');
|
||||
$choose = $ctx->t('Wybierz');
|
||||
|
||||
$html = '';
|
||||
$html .= '<div id="br-picker" class="br-overlay" style="display:none;" data-picker-url="' . $safeUrl . '" data-picker-root="' . $safeRoot . '">';
|
||||
$html .= '<div class="br-modal">';
|
||||
$html .= '<div class="br-modal-header"><div class="br-modal-title">' . AppContext::e($title) . '</div><button class="br-btn br-btn-ghost br-btn-small" type="button" onclick="closePicker();">' . AppContext::e($close) . '</button></div>';
|
||||
$html .= '<div class="br-modal-body">';
|
||||
$html .= '<div class="br-picker-path"><button class="br-btn br-btn-ghost br-btn-small" type="button" onclick="pickerGoUp();">⟵ ' . AppContext::e($up) . '</button><span id="br-picker-current" class="br-muted"></span></div>';
|
||||
$html .= '<div id="br-picker-error" class="br-alert br-alert-error" style="display:none;"></div>';
|
||||
$html .= '<ul id="br-picker-list" class="br-picker-list"></ul>';
|
||||
$html .= '<div id="br-picker-create" class="br-picker-create" style="display:none;">';
|
||||
$html .= '<input id="br-picker-new" type="text" placeholder="' . AppContext::e($newDir) . '" />';
|
||||
$html .= '<button class="br-btn br-btn-ghost br-btn-small" type="button" onclick="createPickerDir();">' . AppContext::e($create) . '</button>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
$html .= '<div class="br-modal-actions">';
|
||||
$html .= '<button class="br-btn br-btn-ghost" type="button" onclick="closePicker();">' . AppContext::e($cancel) . '</button>';
|
||||
$html .= '<button class="br-btn" type="button" onclick="selectPickerPath();">' . AppContext::e($choose) . '</button>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div></div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public static function handleDirList(AppContext $ctx, BackupQueueService $queue, string $rootPath): bool
|
||||
{
|
||||
$action = strtolower($ctx->queryString('action'));
|
||||
if ($action !== 'dir_list') {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
@ob_end_clean();
|
||||
}
|
||||
|
||||
$base = self::normalizeBase($rootPath);
|
||||
$mode = strtolower($ctx->queryString('mode', 'dir')) === 'file' ? 'file' : 'dir';
|
||||
$requested = trim($ctx->queryString('path', $base));
|
||||
if ($requested === '') {
|
||||
$requested = $base;
|
||||
}
|
||||
if ($requested !== '' && $requested[0] !== '/') {
|
||||
$requested = '/' . $requested;
|
||||
}
|
||||
|
||||
$selected = '';
|
||||
if ($mode === 'file' && $requested !== '') {
|
||||
$realReq = @realpath($requested);
|
||||
if ($realReq !== false && is_file($realReq)) {
|
||||
$selected = $realReq;
|
||||
$requested = dirname($realReq);
|
||||
}
|
||||
}
|
||||
|
||||
[$resolved, $base, $ok] = self::resolvePickerPath($requested, $base);
|
||||
if (!$ok) {
|
||||
self::pickerOutput(['ok' => false, 'error' => 'Nieprawidłowa ścieżka.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($base !== '/' && $selected !== '') {
|
||||
if (strpos($selected, $base . '/') !== 0 && $selected !== $base) {
|
||||
$selected = '';
|
||||
}
|
||||
}
|
||||
|
||||
$dirItems = [];
|
||||
$fileItems = [];
|
||||
$entries = @scandir($resolved);
|
||||
if (is_array($entries)) {
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry === '.' || $entry === '..') {
|
||||
continue;
|
||||
}
|
||||
$full = $resolved . '/' . $entry;
|
||||
if (is_dir($full)) {
|
||||
$dirItems[] = $entry;
|
||||
} elseif ($mode === 'file' && is_file($full) && $queue->isAllowedSqlFile($full)) {
|
||||
$fileItems[] = $entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
natcasesort($dirItems);
|
||||
natcasesort($fileItems);
|
||||
$items = [];
|
||||
foreach ($dirItems as $name) {
|
||||
$items[] = ['name' => $name, 'type' => 'dir'];
|
||||
}
|
||||
foreach ($fileItems as $name) {
|
||||
$items[] = ['name' => $name, 'type' => 'file'];
|
||||
}
|
||||
|
||||
self::pickerOutput([
|
||||
'ok' => true,
|
||||
'path' => $resolved,
|
||||
'base' => $base,
|
||||
'items' => $items,
|
||||
'selected' => $selected,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function handleDirCreate(AppContext $ctx, string $rootPath, string $daUser): bool
|
||||
{
|
||||
$action = strtolower($ctx->queryString('action'));
|
||||
if ($action !== 'dir_create') {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
@ob_end_clean();
|
||||
}
|
||||
|
||||
$base = self::normalizeBase($rootPath);
|
||||
$parentReq = trim($ctx->queryString('path', $base));
|
||||
if ($parentReq !== '' && $parentReq[0] !== '/') {
|
||||
$parentReq = '/' . $parentReq;
|
||||
}
|
||||
[$parent, $base, $ok] = self::resolvePickerPath($parentReq, $base);
|
||||
$name = trim($ctx->queryString('name'));
|
||||
if ($name === '' || $name === '.' || $name === '..' || strpos($name, '/') !== false || strpos($name, '\\') !== false) {
|
||||
self::pickerOutput(['ok' => false, 'error' => 'invalid_name']);
|
||||
exit;
|
||||
}
|
||||
if (!$ok || !is_dir($parent)) {
|
||||
self::pickerOutput(['ok' => false, 'error' => 'invalid_parent']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$newPath = rtrim($parent, '/') . '/' . $name;
|
||||
if ($base !== '/' && strpos($newPath, $base . '/') !== 0 && $newPath !== $base) {
|
||||
self::pickerOutput(['ok' => false, 'error' => 'outside_base']);
|
||||
exit;
|
||||
}
|
||||
if (file_exists($newPath)) {
|
||||
self::pickerOutput(['ok' => false, 'error' => 'exists']);
|
||||
exit;
|
||||
}
|
||||
if (!@mkdir($newPath, 0700, true)) {
|
||||
self::pickerOutput(['ok' => false, 'error' => 'mkdir_failed']);
|
||||
exit;
|
||||
}
|
||||
@chown($newPath, $daUser);
|
||||
@chgrp($newPath, $daUser);
|
||||
self::pickerOutput(['ok' => true, 'path' => $parent, 'base' => $base, 'created' => $newPath]);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
*/
|
||||
private static function pickerOutput(array $payload): void
|
||||
{
|
||||
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
if ($json === false) {
|
||||
$json = json_encode(['ok' => false, 'error' => 'json_encode_failed'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
$marker = '__DA_MYSQL_PICKER_JSON__';
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
echo $marker . "\n" . $json . "\n" . $marker;
|
||||
}
|
||||
|
||||
private static function normalizeBase(string $base): string
|
||||
{
|
||||
$base = trim($base);
|
||||
if ($base === '') {
|
||||
return '/';
|
||||
}
|
||||
if ($base[0] !== '/') {
|
||||
$base = '/' . $base;
|
||||
}
|
||||
if ($base !== '/' && substr($base, -1) === '/') {
|
||||
$base = rtrim($base, '/');
|
||||
}
|
||||
return $base === '' ? '/' : $base;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:string,1:string,2:bool}
|
||||
*/
|
||||
private static function resolvePickerPath(string $requested, string $base): array
|
||||
{
|
||||
$base = self::normalizeBase($base);
|
||||
$candidate = trim($requested);
|
||||
if ($candidate === '') {
|
||||
$candidate = $base;
|
||||
}
|
||||
if ($candidate[0] !== '/') {
|
||||
$candidate = '/' . $candidate;
|
||||
}
|
||||
$resolved = @realpath($candidate);
|
||||
if ($resolved === false || !is_dir($resolved)) {
|
||||
$resolved = @realpath($base);
|
||||
}
|
||||
if ($resolved === false || !is_dir($resolved)) {
|
||||
return [$base, $base, false];
|
||||
}
|
||||
if ($base !== '/' && strpos($resolved, $base . '/') !== 0 && $resolved !== $base) {
|
||||
$resolved = @realpath($base);
|
||||
}
|
||||
if ($resolved === false || !is_dir($resolved)) {
|
||||
return [$base, $base, false];
|
||||
}
|
||||
return [$resolved, $base, true];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class Http
|
||||
{
|
||||
public static function bootstrapGlobals(): void
|
||||
{
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
return;
|
||||
}
|
||||
|
||||
$query = getenv('QUERY_STRING') ?: '';
|
||||
$method = strtoupper((string)(getenv('REQUEST_METHOD') ?: 'GET'));
|
||||
|
||||
$_GET = [];
|
||||
if ($query !== '') {
|
||||
parse_str($query, $_GET);
|
||||
}
|
||||
$queryAction = strtolower(trim((string)($_GET['action'] ?? '')));
|
||||
|
||||
$_POST = [];
|
||||
if ($method === 'POST') {
|
||||
$rawPost = (string)(getenv('POST') ?: '');
|
||||
if ($rawPost === '') {
|
||||
$rawPost = (string)(getenv('HTTP_POST') ?: '');
|
||||
}
|
||||
|
||||
$contentType = strtolower((string)(getenv('CONTENT_TYPE') ?: ''));
|
||||
$isUrlEncoded = ($contentType === '' || str_contains($contentType, 'application/x-www-form-urlencoded'));
|
||||
if ($rawPost === '' && $isUrlEncoded) {
|
||||
if ($queryAction !== 'upload_blob') {
|
||||
$stdin = @file_get_contents('php://stdin');
|
||||
if (is_string($stdin) && $stdin !== '') {
|
||||
$rawPost = $stdin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($rawPost !== '') {
|
||||
parse_str($rawPost, $_POST);
|
||||
}
|
||||
}
|
||||
|
||||
$_REQUEST = array_merge($_GET, $_POST);
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
$_SERVER['QUERY_STRING'] = $query;
|
||||
|
||||
$knownServerVars = [
|
||||
'USER',
|
||||
'USERNAME',
|
||||
'HOME',
|
||||
'LANGUAGE',
|
||||
'SESSION_ID',
|
||||
'REQUEST_URI',
|
||||
'HTTP_HOST',
|
||||
'HTTPS',
|
||||
'HTTP_USER_AGENT',
|
||||
'HTTP_X_FORWARDED_PROTO',
|
||||
'HTTP_FRONT_END_HTTPS',
|
||||
'REQUEST_SCHEME',
|
||||
'SERVER_NAME',
|
||||
'SERVER_PORT',
|
||||
'REMOTE_ADDR',
|
||||
];
|
||||
foreach ($knownServerVars as $key) {
|
||||
if (!isset($_SERVER[$key])) {
|
||||
$value = getenv($key);
|
||||
if ($value !== false) {
|
||||
$_SERVER[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function method(): string
|
||||
{
|
||||
return strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
||||
}
|
||||
|
||||
public static function isPost(): bool
|
||||
{
|
||||
return self::method() === 'POST';
|
||||
}
|
||||
|
||||
public static function getString(array $source, string $key, string $default = ''): string
|
||||
{
|
||||
if (!isset($source[$key])) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$value = $source[$key];
|
||||
if (is_array($value)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return trim((string)$value);
|
||||
}
|
||||
|
||||
public static function getArray(array $source, string $key): array
|
||||
{
|
||||
if (!isset($source[$key])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$value = $source[$key];
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public static function server(string $key, string $default = ''): string
|
||||
{
|
||||
$value = $_SERVER[$key] ?? null;
|
||||
if ($value === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return trim((string)$value);
|
||||
}
|
||||
|
||||
public static function redirect(string $url): void
|
||||
{
|
||||
header('Location: ' . $url, true, 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class Lang
|
||||
{
|
||||
/** @var array<string,string> */
|
||||
private array $map;
|
||||
|
||||
/**
|
||||
* @param array<string,string> $map
|
||||
*/
|
||||
private function __construct(array $map)
|
||||
{
|
||||
$this->map = $map;
|
||||
}
|
||||
|
||||
public static function load(string $code): self
|
||||
{
|
||||
$code = strtolower(trim($code));
|
||||
if ($code !== 'pl' && $code !== 'en') {
|
||||
$code = 'en';
|
||||
}
|
||||
|
||||
$path = PLUGIN_ROOT . '/lang/' . $code . '.php';
|
||||
if (!is_file($path)) {
|
||||
$path = PLUGIN_ROOT . '/lang/en.php';
|
||||
}
|
||||
|
||||
$map = [];
|
||||
if (is_file($path)) {
|
||||
$data = require $path;
|
||||
if (is_array($data)) {
|
||||
$map = $data;
|
||||
}
|
||||
}
|
||||
|
||||
return new self($map);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string|int|float> $vars
|
||||
*/
|
||||
public function t(string $key, array $vars = []): string
|
||||
{
|
||||
$text = $this->map[$key] ?? $key;
|
||||
foreach ($vars as $var => $value) {
|
||||
$text = str_replace('{' . $var . '}', (string)$value, $text);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
public function translateHtml(string $html): string
|
||||
{
|
||||
if ($html === '' || empty($this->map)) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$keys = array_keys($this->map);
|
||||
usort($keys, static function (string $a, string $b): int {
|
||||
return strlen($b) <=> strlen($a);
|
||||
});
|
||||
|
||||
$values = [];
|
||||
foreach ($keys as $key) {
|
||||
$values[] = $this->map[$key];
|
||||
}
|
||||
|
||||
return str_replace($keys, $values, $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function keys(): array
|
||||
{
|
||||
return array_keys($this->map);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class LocalizedException extends RuntimeException
|
||||
{
|
||||
/** @var array<string,string|int|float> */
|
||||
private array $vars;
|
||||
|
||||
/**
|
||||
* @param array<string,string|int|float> $vars
|
||||
*/
|
||||
public function __construct(string $key, array $vars = [], int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($key, $code, $previous);
|
||||
$this->vars = $vars;
|
||||
}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return $this->getMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string|int|float>
|
||||
*/
|
||||
public function vars(): array
|
||||
{
|
||||
return $this->vars;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class NamePolicy
|
||||
{
|
||||
public const MAX_IDENTIFIER_LEN = 64;
|
||||
public const MAX_HOST_COMMENT_LEN = 180;
|
||||
|
||||
/**
|
||||
* @var array<string,string>
|
||||
*/
|
||||
public const PRIVILEGE_LABELS = [
|
||||
'ALL' => 'Wszystkie uprawnienia',
|
||||
'SELECT' => 'SELECT',
|
||||
'INSERT' => 'INSERT',
|
||||
'UPDATE' => 'UPDATE',
|
||||
'DELETE' => 'DELETE',
|
||||
'CREATE' => 'CREATE',
|
||||
'ALTER' => 'ALTER',
|
||||
'DROP' => 'DROP',
|
||||
'INDEX' => 'INDEX',
|
||||
'REFERENCES' => 'REFERENCES',
|
||||
'CREATE_VIEW' => 'CREATE VIEW',
|
||||
'SHOW_VIEW' => 'SHOW VIEW',
|
||||
'TRIGGER' => 'TRIGGER',
|
||||
'EXECUTE' => 'EXECUTE',
|
||||
'EVENT' => 'EVENT',
|
||||
'LOCK_TABLES' => 'LOCK TABLES',
|
||||
];
|
||||
|
||||
public static function defaultPrivileges(): array
|
||||
{
|
||||
return ['ALL'];
|
||||
}
|
||||
|
||||
public static function normalizeDatabaseName(string $input, string $prefix): string
|
||||
{
|
||||
return self::normalizeIdentifier($input, $prefix, 'Nazwa bazy');
|
||||
}
|
||||
|
||||
public static function normalizeRoleName(string $input, string $prefix): string
|
||||
{
|
||||
return self::normalizeIdentifier($input, $prefix, 'Nazwa użytkownika');
|
||||
}
|
||||
|
||||
public static function assertBelongsToUser(string $name, string $prefix, string $entityLabel = 'Obiekt'): void
|
||||
{
|
||||
if (strpos($name, $prefix) !== 0) {
|
||||
throw new InvalidArgumentException($entityLabel . ' musi zaczynać się od prefiksu: ' . $prefix);
|
||||
}
|
||||
}
|
||||
|
||||
public static function normalizeIdentifier(string $input, string $prefix, string $label): string
|
||||
{
|
||||
$value = strtolower(trim($input));
|
||||
if ($value === '') {
|
||||
throw new InvalidArgumentException($label . ' jest wymagana.');
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-z0-9_]+$/', $value)) {
|
||||
throw new InvalidArgumentException($label . ' może zawierać wyłącznie znaki a-z, 0-9 oraz _.');
|
||||
}
|
||||
|
||||
if (strpos($value, $prefix) !== 0) {
|
||||
$value = $prefix . $value;
|
||||
}
|
||||
|
||||
if (strlen($value) > self::MAX_IDENTIFIER_LEN) {
|
||||
throw new InvalidArgumentException($label . ' przekracza limit 64 znaków MySQL.');
|
||||
}
|
||||
|
||||
if ($value === $prefix) {
|
||||
throw new InvalidArgumentException($label . ' nie może być równa samemu prefiksowi.');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function parseHosts(string $raw): array
|
||||
{
|
||||
$raw = str_replace(["\r\n", "\r", ';'], ["\n", "\n", "\n"], $raw);
|
||||
$raw = str_replace(',', "\n", $raw);
|
||||
$parts = explode("\n", $raw);
|
||||
|
||||
$hosts = [];
|
||||
foreach ($parts as $part) {
|
||||
$part = trim($part);
|
||||
if ($part === '') {
|
||||
continue;
|
||||
}
|
||||
$hosts[] = $part;
|
||||
}
|
||||
|
||||
return array_values(array_unique($hosts));
|
||||
}
|
||||
|
||||
public static function normalizeHost(string $input, bool $allowWildcard): string
|
||||
{
|
||||
$host = strtolower(trim($input));
|
||||
if ($host === '') {
|
||||
throw new InvalidArgumentException('Pusty host.');
|
||||
}
|
||||
|
||||
if (in_array($host, ['localhost', '127.0.0.1', '::1', '%'], true)) {
|
||||
return $host;
|
||||
}
|
||||
|
||||
if (self::isStrictIpv4($host)) {
|
||||
return $host;
|
||||
}
|
||||
|
||||
if ($allowWildcard && preg_match('/^[A-Za-z0-9._%-]+$/', $host)) {
|
||||
return $host;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Nieobsługiwany format hosta: ' . $input);
|
||||
}
|
||||
|
||||
public static function normalizeRemoteIpv4Host(string $input): string
|
||||
{
|
||||
$host = trim($input);
|
||||
if ($host === '') {
|
||||
throw new InvalidArgumentException('Adres IPv4 jest wymagany.');
|
||||
}
|
||||
|
||||
if (!self::isStrictIpv4($host)) {
|
||||
throw new InvalidArgumentException('Dozwolone są tylko pojedyncze adresy IPv4, np. 203.0.113.10.');
|
||||
}
|
||||
|
||||
return $host;
|
||||
}
|
||||
|
||||
public static function normalizeHostComment(string $input): string
|
||||
{
|
||||
$comment = trim(preg_replace('/[\r\n\t]+/', ' ', $input) ?? '');
|
||||
$comment = preg_replace('/\s+/', ' ', $comment) ?? '';
|
||||
$comment = trim($comment);
|
||||
|
||||
if (strlen($comment) > self::MAX_HOST_COMMENT_LEN) {
|
||||
throw new InvalidArgumentException('Komentarz hosta przekracza limit ' . self::MAX_HOST_COMMENT_LEN . ' znaków.');
|
||||
}
|
||||
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $input
|
||||
* @return string[]
|
||||
*/
|
||||
public static function sanitizePrivileges(array $input): array
|
||||
{
|
||||
$allowed = array_keys(self::PRIVILEGE_LABELS);
|
||||
$result = [];
|
||||
|
||||
foreach ($input as $priv) {
|
||||
$priv = strtoupper(trim((string)$priv));
|
||||
if ($priv === '' || !in_array($priv, $allowed, true)) {
|
||||
continue;
|
||||
}
|
||||
$result[] = $priv;
|
||||
}
|
||||
|
||||
$result = array_values(array_unique($result));
|
||||
if (in_array('ALL', $result, true)) {
|
||||
return ['ALL'];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function isStrictIpv4(string $value): bool
|
||||
{
|
||||
if (!preg_match('/^(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$/', $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class PhpMyAdminRuntimeConfig
|
||||
{
|
||||
private const FILE_NAME = 'config.json';
|
||||
|
||||
public static function sync(Settings $settings, string $pluginErrorLog = ''): void
|
||||
{
|
||||
$runtimeDir = rtrim($settings->adminerSsoDir(), '/');
|
||||
if ($runtimeDir === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_dir($runtimeDir) && !@mkdir($runtimeDir, 0755, true) && !is_dir($runtimeDir)) {
|
||||
self::log($pluginErrorLog, 'Cannot create phpMyAdmin runtime dir: ' . $runtimeDir);
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'version' => 1,
|
||||
'phpmyadmin_url_path' => $settings->adminerUrlPath(),
|
||||
'phpmyadmin_public_base_url' => $settings->adminerPublicBaseUrl(),
|
||||
'ticket_dir' => $runtimeDir . '/tickets',
|
||||
'host' => 'dynamic:' . basename($settings->mysqlCredentialsFile()),
|
||||
'credentials_file' => $settings->mysqlCredentialsFile(),
|
||||
'client_bin_dir' => $settings->mysqlClientBinDir(),
|
||||
'updated_at' => time(),
|
||||
];
|
||||
|
||||
$encoded = json_encode($payload, JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($encoded) || $encoded === '') {
|
||||
self::log($pluginErrorLog, 'Failed to encode phpMyAdmin runtime config JSON.');
|
||||
return;
|
||||
}
|
||||
|
||||
$targetPath = $runtimeDir . '/' . self::FILE_NAME;
|
||||
if (@file_put_contents($targetPath, $encoded) === false) {
|
||||
self::log($pluginErrorLog, 'Failed to write phpMyAdmin runtime config: ' . $targetPath);
|
||||
return;
|
||||
}
|
||||
|
||||
@chmod($targetPath, 0644);
|
||||
}
|
||||
|
||||
private static function log(string $pluginErrorLog, string $message): void
|
||||
{
|
||||
if ($pluginErrorLog === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$line = sprintf("[%s] PHPMYADMIN_RUNTIME_CONFIG %s\n", date('c'), $message);
|
||||
@error_log($line, 3, $pluginErrorLog);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class PhpMyAdminSso
|
||||
{
|
||||
private Settings $settings;
|
||||
private DirectAdminUser $daUser;
|
||||
private MySQLService $db;
|
||||
|
||||
public function __construct(Settings $settings, DirectAdminUser $daUser, MySQLService $db)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->daUser = $daUser;
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{target:string,auth:array<string,string|int>}
|
||||
*/
|
||||
public function issueLoginPayload(?string $requestedDatabase = null): array
|
||||
{
|
||||
if (!$this->settings->enableAdminer()) {
|
||||
throw new RuntimeException('Integracja phpMyAdmin jest wyłączona.');
|
||||
}
|
||||
|
||||
$this->db->cleanupExpiredAdminerRoles();
|
||||
|
||||
$databaseRows = $this->db->listDatabasesForUser($this->daUser->username(), false);
|
||||
if (empty($databaseRows)) {
|
||||
throw new RuntimeException('Użytkownik nie ma żadnej bazy MySQL do otwarcia w phpMyAdmin.');
|
||||
}
|
||||
|
||||
$databaseNames = [];
|
||||
foreach ($databaseRows as $row) {
|
||||
$name = (string)($row['name'] ?? '');
|
||||
if ($name !== '') {
|
||||
$databaseNames[] = $name;
|
||||
}
|
||||
}
|
||||
$databaseNames = array_values(array_unique($databaseNames));
|
||||
|
||||
$targetDatabase = '';
|
||||
if ($requestedDatabase !== null) {
|
||||
$requestedDatabase = trim($requestedDatabase);
|
||||
if ($requestedDatabase !== '') {
|
||||
if (!in_array($requestedDatabase, $databaseNames, true)) {
|
||||
throw new RuntimeException('Wskazana baza danych nie należy do zalogowanego użytkownika.');
|
||||
}
|
||||
$targetDatabase = $requestedDatabase;
|
||||
}
|
||||
}
|
||||
if ($targetDatabase === '') {
|
||||
$targetDatabase = $databaseNames[0];
|
||||
}
|
||||
|
||||
$roleTtl = max($this->settings->adminerRoleTtl(), $this->settings->adminerSessionTtl() + 120);
|
||||
$role = $this->generateTemporaryRoleName();
|
||||
$password = $this->generateSecret(32);
|
||||
$expiresAt = time() + $roleTtl;
|
||||
|
||||
$this->db->createTemporaryRole($role, $password, $expiresAt);
|
||||
try {
|
||||
foreach ($databaseNames as $dbName) {
|
||||
$this->db->grantTemporaryAdminerAccess($dbName, $role);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
try {
|
||||
$this->db->dropTemporaryRole($role);
|
||||
} catch (Throwable $ignored) {
|
||||
}
|
||||
throw new RuntimeException('Nie udało się przygotować sesji phpMyAdmin: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
$auth = [
|
||||
'user' => $role,
|
||||
'password' => $password,
|
||||
'host' => $this->db->connectionEndpoint()['host'],
|
||||
'port' => $this->db->connectionEndpoint()['port'],
|
||||
'db' => $targetDatabase,
|
||||
];
|
||||
$ticket = $this->writeLoginTicket($auth, $targetDatabase);
|
||||
|
||||
return [
|
||||
'target' => $this->buildPhpMyAdminLoginUrl($targetDatabase, $ticket),
|
||||
'auth' => $auth,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildPhpMyAdminLoginUrl(string $database, string $ticket): string
|
||||
{
|
||||
$query = http_build_query([
|
||||
'ticket' => $ticket,
|
||||
'route' => '/database/structure',
|
||||
'db' => $database,
|
||||
]);
|
||||
|
||||
return $this->phpMyAdminBaseUrl() . '/da_login.php?' . $query;
|
||||
}
|
||||
|
||||
private function phpMyAdminBaseUrl(): string
|
||||
{
|
||||
$path = $this->settings->adminerUrlPath();
|
||||
$customBase = $this->settings->adminerPublicBaseUrl();
|
||||
if ($customBase !== '') {
|
||||
return $customBase . $path;
|
||||
}
|
||||
|
||||
$scheme = 'http';
|
||||
$https = strtolower(Http::server('HTTPS'));
|
||||
if ($https !== '' && $https !== 'off' && $https !== '0') {
|
||||
$scheme = 'https';
|
||||
} elseif (strtolower(Http::server('HTTP_X_FORWARDED_PROTO')) === 'https') {
|
||||
$scheme = 'https';
|
||||
} elseif (strtolower(Http::server('REQUEST_SCHEME')) === 'https') {
|
||||
$scheme = 'https';
|
||||
}
|
||||
|
||||
$host = Http::server('HTTP_HOST');
|
||||
if ($host === '') {
|
||||
$host = Http::server('SERVER_NAME', 'localhost');
|
||||
}
|
||||
$host = preg_replace('/:\d+$/', '', $host) ?? $host;
|
||||
|
||||
return $scheme . '://' . $host . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string|int> $auth
|
||||
*/
|
||||
private function writeLoginTicket(array $auth, string $database): string
|
||||
{
|
||||
$ticketDir = rtrim($this->settings->adminerSsoDir(), '/') . '/tickets';
|
||||
if (!is_dir($ticketDir) && !@mkdir($ticketDir, 0755, true) && !is_dir($ticketDir)) {
|
||||
throw new RuntimeException('Nie można utworzyć katalogu ticketów phpMyAdmin: ' . $ticketDir);
|
||||
}
|
||||
|
||||
$this->cleanupExpiredTickets($ticketDir);
|
||||
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$payload = [
|
||||
'version' => 1,
|
||||
'created_at' => time(),
|
||||
'expires_at' => time() + $this->settings->adminerTicketTtl(),
|
||||
'auth' => $auth,
|
||||
'route' => '/database/structure',
|
||||
'db' => $database,
|
||||
];
|
||||
$encoded = json_encode($payload, JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($encoded) || $encoded === '') {
|
||||
throw new RuntimeException('Nie udało się zakodować ticketu phpMyAdmin.');
|
||||
}
|
||||
|
||||
$path = $ticketDir . '/' . $token . '.json';
|
||||
$tmpPath = $path . '.tmp.' . bin2hex(random_bytes(4));
|
||||
if (@file_put_contents($tmpPath, $encoded, LOCK_EX) === false) {
|
||||
throw new RuntimeException('Nie udało się zapisać ticketu phpMyAdmin.');
|
||||
}
|
||||
@chmod($tmpPath, 0644);
|
||||
if (!@rename($tmpPath, $path)) {
|
||||
@unlink($tmpPath);
|
||||
throw new RuntimeException('Nie udało się aktywować ticketu phpMyAdmin.');
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
private function cleanupExpiredTickets(string $ticketDir): void
|
||||
{
|
||||
foreach (glob($ticketDir . '/*.json') ?: [] as $path) {
|
||||
if (!is_file($path)) {
|
||||
continue;
|
||||
}
|
||||
$raw = @file_get_contents($path);
|
||||
$decoded = is_string($raw) ? json_decode($raw, true) : null;
|
||||
$expiresAt = is_array($decoded) ? (int)($decoded['expires_at'] ?? 0) : 0;
|
||||
if ($expiresAt > 0 && $expiresAt >= time()) {
|
||||
continue;
|
||||
}
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
private function generateTemporaryRoleName(): string
|
||||
{
|
||||
$base = strtolower($this->daUser->username());
|
||||
$base = preg_replace('/[^a-z0-9_]+/', '_', $base) ?? 'user';
|
||||
$base = trim($base, '_');
|
||||
if ($base === '') {
|
||||
$base = 'user';
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$suffix = bin2hex(random_bytes(6));
|
||||
$maxBaseLen = NamePolicy::MAX_IDENTIFIER_LEN - strlen('da_tmp_phpmyadmin_') - 1 - strlen($suffix);
|
||||
if ($maxBaseLen < 1) {
|
||||
$maxBaseLen = 1;
|
||||
}
|
||||
$safeBase = substr($base, 0, $maxBaseLen);
|
||||
$role = 'da_tmp_phpmyadmin_' . $safeBase . '_' . $suffix;
|
||||
if (!$this->db->roleExists($role)) {
|
||||
return $role;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('Nie udało się wygenerować unikalnego tymczasowego użytkownika dla phpMyAdmin.');
|
||||
}
|
||||
|
||||
private function generateSecret(int $len): string
|
||||
{
|
||||
$value = rtrim(strtr(base64_encode(random_bytes(48)), '+/', 'AZ'), '=');
|
||||
if (strlen($value) >= $len) {
|
||||
return substr($value, 0, $len);
|
||||
}
|
||||
return str_pad($value, $len, 'x');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class Settings
|
||||
{
|
||||
/** @var array<string,string> */
|
||||
private array $values;
|
||||
|
||||
private function __construct(array $values)
|
||||
{
|
||||
$this->values = $values;
|
||||
}
|
||||
|
||||
public static function load(string $path): self
|
||||
{
|
||||
$values = [];
|
||||
if (is_readable($path)) {
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES);
|
||||
if ($lines !== false) {
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!preg_match('/^([A-Za-z0-9_]+)=(.*)$/', $line, $m)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values[$m[1]] = self::parseShValue((string)$m[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new self($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{host:string,port:int,database:string,user:string,password:string,socket:string}
|
||||
*/
|
||||
public static function loadMysqlCredentials(string $path): array
|
||||
{
|
||||
if (!is_readable($path)) {
|
||||
throw new RuntimeException('Brak pliku z danymi MySQL: ' . $path);
|
||||
}
|
||||
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES);
|
||||
if ($lines === false) {
|
||||
throw new RuntimeException('Nie można odczytać pliku: ' . $path);
|
||||
}
|
||||
|
||||
$kv = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strpos($line, '=') !== false) {
|
||||
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||
if ($k !== '') {
|
||||
$kv[strtoupper($k)] = self::parseShValue($v);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = explode(':', $line);
|
||||
if (count($parts) >= 5) {
|
||||
return [
|
||||
'host' => trim($parts[0]) !== '' ? trim($parts[0]) : 'localhost',
|
||||
'port' => (int)(trim($parts[1]) !== '' ? trim($parts[1]) : '3306'),
|
||||
'database' => trim($parts[2]) !== '' ? trim($parts[2]) : 'mysql',
|
||||
'user' => trim($parts[3]) !== '' ? trim($parts[3]) : 'root',
|
||||
'password' => trim(implode(':', array_slice($parts, 4))),
|
||||
'socket' => '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$host = trim((string)($kv['HOST'] ?? $kv['MYSQL_HOST'] ?? ''));
|
||||
if ($host === '') {
|
||||
$host = 'localhost';
|
||||
}
|
||||
|
||||
$portRaw = trim((string)($kv['PORT'] ?? $kv['MYSQL_PORT'] ?? '3306'));
|
||||
$port = preg_match('/^[0-9]+$/', $portRaw) ? (int)$portRaw : 3306;
|
||||
if ($port < 1 || $port > 65535) {
|
||||
$port = 3306;
|
||||
}
|
||||
|
||||
$database = trim((string)($kv['DATABASE'] ?? $kv['MYSQL_DATABASE'] ?? $kv['DB'] ?? 'mysql'));
|
||||
if ($database === '' || $database === '*') {
|
||||
$database = 'mysql';
|
||||
}
|
||||
|
||||
$user = trim((string)($kv['USER'] ?? $kv['MYSQL_USER'] ?? 'root'));
|
||||
$password = trim((string)($kv['PASSWORD'] ?? $kv['PASSWD'] ?? $kv['MYSQL_PASSWORD'] ?? ''));
|
||||
$socket = trim((string)($kv['SOCKET'] ?? $kv['MYSQL_SOCKET'] ?? ''));
|
||||
|
||||
if ($user === '') {
|
||||
throw new RuntimeException('Brak użytkownika MySQL w pliku: ' . $path);
|
||||
}
|
||||
|
||||
return [
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'database' => $database,
|
||||
'user' => $user,
|
||||
'password' => $password,
|
||||
'socket' => $socket,
|
||||
];
|
||||
}
|
||||
|
||||
public function mysqlCredentialsFile(): string
|
||||
{
|
||||
$path = trim($this->getString('MYSQL_PLUGIN_CREDENTIALS_FILE', '/usr/local/directadmin/conf/alt-mysql.conf'));
|
||||
return $path !== '' ? $path : '/usr/local/directadmin/conf/alt-mysql.conf';
|
||||
}
|
||||
|
||||
public function mysqlClientBinDir(): string
|
||||
{
|
||||
return rtrim(trim($this->getString('MYSQL_PLUGIN_CLIENT_BIN_DIR', '')), '/');
|
||||
}
|
||||
|
||||
public function getString(string $key, string $default = ''): string
|
||||
{
|
||||
return isset($this->values[$key]) ? trim($this->values[$key]) : $default;
|
||||
}
|
||||
|
||||
public function getInt(string $key, int $default = 0): int
|
||||
{
|
||||
if (!isset($this->values[$key])) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$value = trim($this->values[$key]);
|
||||
if ($value === '' || !preg_match('/^-?[0-9]+$/', $value)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
public function getBool(string $key, bool $default = false): bool
|
||||
{
|
||||
if (!isset($this->values[$key])) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return self::toBool($this->values[$key], $default);
|
||||
}
|
||||
|
||||
public function defaultDatabasesLimit(): int
|
||||
{
|
||||
$limit = $this->getInt('MYSQL_PLUGIN_DEFAULT_DB_LIMIT', 5);
|
||||
return $limit < 0 ? 0 : $limit;
|
||||
}
|
||||
|
||||
public function alwaysEnabled(): bool
|
||||
{
|
||||
return $this->getBool('always_enabled', true);
|
||||
}
|
||||
|
||||
public function maxHostsPerUser(): int
|
||||
{
|
||||
$limit = $this->getInt('MYSQL_PLUGIN_MAX_HOSTS_PER_USER', 30);
|
||||
return $limit < 1 ? 1 : $limit;
|
||||
}
|
||||
|
||||
public function allowRemoteHosts(): bool
|
||||
{
|
||||
if (array_key_exists('allow_remote_hosts', $this->values)) {
|
||||
return self::toBool($this->values['allow_remote_hosts'], false);
|
||||
}
|
||||
|
||||
if (array_key_exists('MYSQL_PLUGIN_ALLOW_REMOTE_HOSTS', $this->values)) {
|
||||
return self::toBool($this->values['MYSQL_PLUGIN_ALLOW_REMOTE_HOSTS'], false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function modifyServerConfiguration(): bool
|
||||
{
|
||||
if ($this->allowRemoteHosts()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (array_key_exists('modify_server_configuration', $this->values)) {
|
||||
return self::toBool($this->values['modify_server_configuration'], false);
|
||||
}
|
||||
|
||||
if (array_key_exists('MYSQL_PLUGIN_MODIFY_SERVER_CONFIGURATION', $this->values)) {
|
||||
return self::toBool($this->values['MYSQL_PLUGIN_MODIFY_SERVER_CONFIGURATION'], false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hostSyncScript(): string
|
||||
{
|
||||
$default = '/usr/local/directadmin/plugins/alt-mysql/scripts/setup/mysql_host_sync.sh';
|
||||
$path = $this->getString('MYSQL_PLUGIN_SUDO_SYNC_HOSTS', $default);
|
||||
return $path !== '' ? $path : $default;
|
||||
}
|
||||
|
||||
public function enableBackup(): bool
|
||||
{
|
||||
return $this->getBool('ENABLE_BACKUP', true);
|
||||
}
|
||||
|
||||
public function enableUploadBackup(): bool
|
||||
{
|
||||
return $this->getBool('ENABLE_UPLOAD_BACKUP', true);
|
||||
}
|
||||
|
||||
public function enableAdminer(): bool
|
||||
{
|
||||
if (!$this->getBool('ENABLE_PHPMYADMIN', $this->getBool('ENABLE_ADMINER', true))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isPhpMyAdminInstalled();
|
||||
}
|
||||
|
||||
public function phpMyAdminInstallDir(): string
|
||||
{
|
||||
$path = trim($this->getString('PHPMYADMIN_INSTALL_DIR', '/var/www/html/alt-mysql-phpmyadmin'));
|
||||
return $path !== '' ? rtrim($path, '/') : '/var/www/html/alt-mysql-phpmyadmin';
|
||||
}
|
||||
|
||||
public function adminerUrlPath(): string
|
||||
{
|
||||
$path = trim($this->getString('PHPMYADMIN_URL_PATH', $this->getString('ADMINER_URL_PATH', '/alt-mysql-phpmyadmin')));
|
||||
if ($path === '') {
|
||||
return '/alt-mysql-phpmyadmin';
|
||||
}
|
||||
|
||||
if ($path[0] !== '/') {
|
||||
$path = '/' . $path;
|
||||
}
|
||||
|
||||
$path = rtrim($path, '/');
|
||||
return $path !== '' ? $path : '/alt-mysql-phpmyadmin';
|
||||
}
|
||||
|
||||
public function adminerPublicBaseUrl(): string
|
||||
{
|
||||
$url = trim($this->getString('PHPMYADMIN_PUBLIC_BASE_URL', $this->getString('ADMINER_PUBLIC_BASE_URL', '')));
|
||||
if ($url === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$url = rtrim($url, '/');
|
||||
if (!preg_match('#^https?://#i', $url)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function adminerPublic(): bool
|
||||
{
|
||||
return $this->getBool('PHPMYADMIN_PUBLIC', $this->getBool('ADMINER_PUBLIC', false));
|
||||
}
|
||||
|
||||
public function adminerSsoDir(): string
|
||||
{
|
||||
$path = trim($this->getString('PHPMYADMIN_SSO_DIR', $this->getString('ADMINER_SSO_DIR', $this->phpMyAdminInstallDir() . '/runtime')));
|
||||
if ($path === '') {
|
||||
$path = $this->phpMyAdminInstallDir() . '/runtime';
|
||||
}
|
||||
return rtrim($path, '/');
|
||||
}
|
||||
|
||||
public function adminerTicketTtl(): int
|
||||
{
|
||||
$ttl = $this->getInt('PHPMYADMIN_TICKET_TTL', $this->getInt('ADMINER_TICKET_TTL', 120));
|
||||
if ($ttl < 10) {
|
||||
return 10;
|
||||
}
|
||||
if ($ttl > 300) {
|
||||
return 300;
|
||||
}
|
||||
return $ttl;
|
||||
}
|
||||
|
||||
public function adminerSessionTtl(): int
|
||||
{
|
||||
$ttl = $this->getInt('PHPMYADMIN_SESSION_TTL', $this->getInt('ADMINER_SESSION_TTL', 900));
|
||||
if ($ttl < 60) {
|
||||
return 60;
|
||||
}
|
||||
if ($ttl > 3600) {
|
||||
return 3600;
|
||||
}
|
||||
return $ttl;
|
||||
}
|
||||
|
||||
public function adminerRoleTtl(): int
|
||||
{
|
||||
$ttl = $this->getInt('PHPMYADMIN_ROLE_TTL', $this->getInt('ADMINER_ROLE_TTL', 1200));
|
||||
if ($ttl < 120) {
|
||||
return 120;
|
||||
}
|
||||
if ($ttl > 7200) {
|
||||
return 7200;
|
||||
}
|
||||
return $ttl;
|
||||
}
|
||||
|
||||
public function phpMyAdminIntegrationWarning(): string
|
||||
{
|
||||
if (!$this->getBool('ENABLE_PHPMYADMIN', $this->getBool('ENABLE_ADMINER', true))) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$dir = $this->phpMyAdminInstallDir();
|
||||
if (!is_dir($dir)) {
|
||||
return 'Prywatna kopia phpMyAdmin dla pluginu nie jest zainstalowana. Uruchom scripts/setup/phpmyadmin_private_install.sh jako root.';
|
||||
}
|
||||
|
||||
$config = $dir . '/config.inc.php';
|
||||
$signon = $dir . '/da_signon.php';
|
||||
$signonUrl = $dir . '/da_login.php';
|
||||
|
||||
if (!is_readable($config)) {
|
||||
return 'Prywatny phpMyAdmin pluginu jest zainstalowany, ale nie można odczytać config.inc.php.';
|
||||
}
|
||||
|
||||
if (!is_readable($signon)) {
|
||||
return 'Prywatny phpMyAdmin pluginu jest zainstalowany, ale brakuje da_signon.php. Uruchom scripts/setup/phpmyadmin_repair.sh jako root.';
|
||||
}
|
||||
if (!is_readable($signonUrl)) {
|
||||
return 'Prywatny phpMyAdmin pluginu jest zainstalowany, ale brakuje da_login.php. Uruchom scripts/setup/phpmyadmin_repair.sh jako root.';
|
||||
}
|
||||
|
||||
$contents = file_get_contents($config);
|
||||
if (!is_string($contents) || strpos($contents, 'DA_MYSQL_PMA_DEFAULT_CONF') === false) {
|
||||
return 'Prywatny phpMyAdmin pluginu nie ma konfiguracji alt-mysql. Uruchom scripts/setup/phpmyadmin_repair.sh jako root.';
|
||||
}
|
||||
if (strpos($contents, "SignonURL'] = da_mysql_phpmyadmin_signon_url") === false) {
|
||||
return 'Prywatny phpMyAdmin pluginu nie ma wymaganej konfiguracji SignonURL. Uruchom scripts/setup/phpmyadmin_repair.sh jako root.';
|
||||
}
|
||||
$signonUrlContents = file_get_contents($signonUrl);
|
||||
if (!is_string($signonUrlContents) || strpos($signonUrlContents, 'da_mysql_login_consume_ticket') === false) {
|
||||
return 'Prywatny phpMyAdmin pluginu nie obsługuje jednorazowych ticketów SSO. Uruchom scripts/setup/phpmyadmin_repair.sh jako root.';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function allowRestoreToOtherDatabase(): bool
|
||||
{
|
||||
return $this->getBool('ALLOW_RESTORE_TO_OTHER_DATABASE', true);
|
||||
}
|
||||
|
||||
public function enableTableCount(): bool
|
||||
{
|
||||
return $this->getBool('ENABLE_TABLE_COUNT', true);
|
||||
}
|
||||
|
||||
public function countDatabaseSize(): bool
|
||||
{
|
||||
return $this->getBool('COUNT_DATABASE_SIZE', true);
|
||||
}
|
||||
|
||||
public function compressBackup(): bool
|
||||
{
|
||||
return $this->getBool('COMPRESS_BACKUP', true);
|
||||
}
|
||||
|
||||
public function hitmeBackupLocation(string $daUser = ''): string
|
||||
{
|
||||
$path = $this->getString('HITME_BACKUP_LOCATION', '/home/admin/mysql_backup');
|
||||
if ($path === '') {
|
||||
$path = '/home/admin/mysql_backup';
|
||||
}
|
||||
|
||||
if ($daUser !== '') {
|
||||
$path = str_replace(['DA_USER', 'da_user'], $daUser, $path);
|
||||
}
|
||||
|
||||
return rtrim($path, '/');
|
||||
}
|
||||
|
||||
public function userBaseBackupDir(string $daUser = ''): string
|
||||
{
|
||||
$path = $this->getString('USER_BASE_BACKUP_DIR', '/home/DA_USER/mysql_backup');
|
||||
if ($path === '') {
|
||||
$path = '/home/DA_USER/mysql_backup';
|
||||
}
|
||||
|
||||
if ($daUser !== '') {
|
||||
$path = str_replace(['DA_USER', 'da_user'], $daUser, $path);
|
||||
}
|
||||
|
||||
return rtrim($path, '/');
|
||||
}
|
||||
|
||||
public function userBackupDateFormat(): string
|
||||
{
|
||||
$format = $this->getString('USER_BACKUP_DATE_FORMAT', 'd.m.Y-H.i');
|
||||
return $format !== '' ? $format : 'd.m.Y-H.i';
|
||||
}
|
||||
|
||||
public function loadOrCreateSecret(string $path): string
|
||||
{
|
||||
if (is_readable($path)) {
|
||||
$value = trim((string)file_get_contents($path));
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
$secret = bin2hex(random_bytes(32));
|
||||
$dir = dirname($path);
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0700, true);
|
||||
}
|
||||
|
||||
if (@file_put_contents($path, $secret) !== false) {
|
||||
@chmod($path, 0600);
|
||||
return $secret;
|
||||
}
|
||||
|
||||
return hash('sha256', __FILE__ . '|' . php_uname('n') . '|' . $path . '|alt-mysql');
|
||||
}
|
||||
|
||||
public static function toBool(string $value, bool $default = false): bool
|
||||
{
|
||||
$v = strtolower(trim($value));
|
||||
if ($v === '') {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if (in_array($v, ['1', 'true', 'yes', 'y', 'on', 'checked'], true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (in_array($v, ['0', 'false', 'no', 'n', 'off', 'unchecked'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
private function isPhpMyAdminInstalled(): bool
|
||||
{
|
||||
$dir = $this->phpMyAdminInstallDir();
|
||||
return is_dir($dir) && is_readable($dir . '/index.php') && is_readable($dir . '/config.inc.php');
|
||||
}
|
||||
|
||||
private static function parseShValue(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($value[0] === '\'' && substr($value, -1) === '\'') {
|
||||
return substr($value, 1, -1);
|
||||
}
|
||||
|
||||
if ($value[0] === '"' && substr($value, -1) === '"') {
|
||||
return stripcslashes(substr($value, 1, -1));
|
||||
}
|
||||
|
||||
$value = preg_replace('/\s+#.*$/', '', $value);
|
||||
return trim((string)$value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user