settings = $settings; $this->daUser = $daUser; $this->db = $db; } /** * @return array{target:string,auth:array} */ 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 $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'); } }