Import postgresql plugin
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class AdminerRuntimeConfig
|
||||
{
|
||||
private const FILE_NAME = 'config.json';
|
||||
|
||||
public static function sync(Settings $settings, string $pluginErrorLog = ''): void
|
||||
{
|
||||
$runtimeDir = rtrim($settings->adminerSsoDir(), '/');
|
||||
if ($runtimeDir === '' || !is_dir($runtimeDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_writable($runtimeDir)) {
|
||||
self::log($pluginErrorLog, 'Adminer runtime dir is not writable: ' . $runtimeDir);
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'version' => 1,
|
||||
'adminer_public' => ($settings->enableAdminer() && $settings->adminerPublic()),
|
||||
'updated_at' => time(),
|
||||
];
|
||||
|
||||
$encoded = json_encode($payload, JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($encoded) || $encoded === '') {
|
||||
self::log($pluginErrorLog, 'Failed to encode Adminer runtime config JSON.');
|
||||
return;
|
||||
}
|
||||
|
||||
$targetPath = $runtimeDir . '/' . self::FILE_NAME;
|
||||
$tempPath = $runtimeDir . '/.' . self::FILE_NAME . '.' . getmypid() . '.tmp';
|
||||
|
||||
$written = @file_put_contents($tempPath, $encoded, LOCK_EX);
|
||||
if ($written === false) {
|
||||
self::log($pluginErrorLog, 'Failed to write temporary Adminer runtime config: ' . $tempPath);
|
||||
return;
|
||||
}
|
||||
|
||||
@chmod($tempPath, 0644);
|
||||
|
||||
if (!@rename($tempPath, $targetPath)) {
|
||||
@unlink($tempPath);
|
||||
self::log($pluginErrorLog, 'Failed to replace Adminer runtime config: ' . $targetPath);
|
||||
return;
|
||||
}
|
||||
|
||||
@chmod($targetPath, 0644);
|
||||
}
|
||||
|
||||
private static function log(string $pluginErrorLog, string $message): void
|
||||
{
|
||||
if ($pluginErrorLog === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$line = sprintf("[%s] ADMINER_RUNTIME_CONFIG %s\n", date('c'), $message);
|
||||
@error_log($line, 3, $pluginErrorLog);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class AdminerSso
|
||||
{
|
||||
private const TMP_ROLE_PREFIX = 'da_tmp_adminer_';
|
||||
|
||||
private Settings $settings;
|
||||
private DirectAdminUser $daUser;
|
||||
private PostgresService $pg;
|
||||
private string $runtimeDir;
|
||||
private string $ticketsDir;
|
||||
|
||||
public function __construct(Settings $settings, DirectAdminUser $daUser, PostgresService $pg)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->daUser = $daUser;
|
||||
$this->pg = $pg;
|
||||
$this->runtimeDir = rtrim($settings->adminerSsoDir(), '/');
|
||||
$this->ticketsDir = $this->runtimeDir . '/tickets';
|
||||
}
|
||||
|
||||
public function issueLoginUrl(?string $requestedDatabase = null): string
|
||||
{
|
||||
if (!$this->settings->enableAdminer()) {
|
||||
throw new RuntimeException('Integracja Adminera jest wyłączona.');
|
||||
}
|
||||
|
||||
$this->assertRuntimeReady();
|
||||
$now = time();
|
||||
$this->cleanupTicketFiles($now);
|
||||
$this->pg->cleanupExpiredAdminerRoles(self::TMP_ROLE_PREFIX);
|
||||
|
||||
$databaseRows = $this->pg->listDatabasesForUser($this->daUser->username(), false);
|
||||
if (empty($databaseRows)) {
|
||||
throw new RuntimeException('Użytkownik nie ma żadnej bazy PostgreSQL do otwarcia w Adminerze.');
|
||||
}
|
||||
|
||||
$databaseNames = [];
|
||||
foreach ($databaseRows as $row) {
|
||||
$name = (string)($row['name'] ?? '');
|
||||
if ($name !== '') {
|
||||
$databaseNames[] = $name;
|
||||
}
|
||||
}
|
||||
$databaseNames = array_values(array_unique($databaseNames));
|
||||
if (empty($databaseNames)) {
|
||||
throw new RuntimeException('Nie znaleziono poprawnych nazw baz danych dla użytkownika.');
|
||||
}
|
||||
|
||||
$ticketTtl = $this->settings->adminerTicketTtl();
|
||||
$sessionTtl = $this->settings->adminerSessionTtl();
|
||||
$roleTtl = max($this->settings->adminerRoleTtl(), $sessionTtl + 120);
|
||||
|
||||
$ticketExpiresAt = $now + $ticketTtl;
|
||||
$sessionExpiresAt = $now + $sessionTtl;
|
||||
$roleExpiresAt = $now + $roleTtl;
|
||||
|
||||
$role = $this->generateTemporaryRoleName();
|
||||
$password = $this->generateSecret(36);
|
||||
$endpoint = $this->pg->connectionEndpoint();
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
$createdRole = false;
|
||||
try {
|
||||
$this->pg->createTemporaryRole($role, $password, $roleExpiresAt);
|
||||
$createdRole = true;
|
||||
|
||||
foreach ($databaseNames as $dbName) {
|
||||
$this->pg->grantTemporaryAdminerAccess($dbName, $role);
|
||||
}
|
||||
|
||||
$ticketId = $this->generateTicketId();
|
||||
$payload = [
|
||||
'v' => 1,
|
||||
'da_user' => $this->daUser->username(),
|
||||
'host' => (string)($endpoint['host'] ?? 'localhost'),
|
||||
'port' => (int)($endpoint['port'] ?? 5432),
|
||||
'role' => $role,
|
||||
'password' => $password,
|
||||
'database' => $targetDatabase,
|
||||
'issued_at' => $now,
|
||||
'expires_at' => $ticketExpiresAt,
|
||||
'session_expires_at' => $sessionExpiresAt,
|
||||
'role_expires_at' => $roleExpiresAt,
|
||||
'user_binding' => $this->buildUserBinding(),
|
||||
];
|
||||
$this->writeTicket($ticketId, $payload);
|
||||
|
||||
$targetBase = $this->buildAdminerBaseUrl();
|
||||
$separator = str_contains($targetBase, '?') ? '&' : '?';
|
||||
return $targetBase . $separator . 'ticket=' . rawurlencode($ticketId);
|
||||
} catch (Throwable $e) {
|
||||
if ($createdRole) {
|
||||
try {
|
||||
$this->pg->dropTemporaryRole($role);
|
||||
} catch (Throwable $cleanupError) {
|
||||
// Ignorujemy błąd cleanupu roli, aby zwrócić pierwotny wyjątek.
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('Nie udało się przygotować sesji Adminera: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function assertRuntimeReady(): void
|
||||
{
|
||||
if (!is_dir($this->runtimeDir)) {
|
||||
throw new RuntimeException('Brak katalogu runtime Adminera: ' . $this->runtimeDir . '. Uruchom scripts/setup/adminer_install.sh');
|
||||
}
|
||||
|
||||
if (!is_dir($this->ticketsDir)) {
|
||||
throw new RuntimeException('Brak katalogu ticketów Adminera: ' . $this->ticketsDir . '. Uruchom scripts/setup/adminer_install.sh');
|
||||
}
|
||||
|
||||
if (!is_readable($this->ticketsDir) || !is_writable($this->ticketsDir)) {
|
||||
throw new RuntimeException('Brak uprawnień odczytu/zapisu do katalogu ticketów: ' . $this->ticketsDir);
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupTicketFiles(int $now): void
|
||||
{
|
||||
$files = glob($this->ticketsDir . '/*.json');
|
||||
if ($files === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hardTtl = max($this->settings->adminerRoleTtl(), 3600);
|
||||
foreach ($files as $path) {
|
||||
if (!is_file($path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$remove = false;
|
||||
$raw = @file_get_contents($path);
|
||||
if (is_string($raw) && $raw !== '') {
|
||||
$data = json_decode($raw, true);
|
||||
if (is_array($data) && isset($data['expires_at']) && is_numeric($data['expires_at'])) {
|
||||
$expiresAt = (int)$data['expires_at'];
|
||||
if ($expiresAt < ($now - 60)) {
|
||||
$remove = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$remove) {
|
||||
$mtime = @filemtime($path);
|
||||
if ($mtime !== false && $mtime < ($now - $hardTtl)) {
|
||||
$remove = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($remove) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
*/
|
||||
private function writeTicket(string $ticketId, array $payload): void
|
||||
{
|
||||
$path = $this->ticketsDir . '/' . $ticketId . '.json';
|
||||
$encoded = json_encode($payload, JSON_UNESCAPED_SLASHES);
|
||||
if ($encoded === false) {
|
||||
throw new RuntimeException('Nie udało się zakodować ticketu Adminera.');
|
||||
}
|
||||
|
||||
$handle = @fopen($path, 'xb');
|
||||
if (!is_resource($handle)) {
|
||||
throw new RuntimeException('Nie udało się utworzyć ticketu Adminera: ' . $path);
|
||||
}
|
||||
|
||||
$written = @fwrite($handle, $encoded);
|
||||
@fclose($handle);
|
||||
if ($written === false || $written < strlen($encoded)) {
|
||||
@unlink($path);
|
||||
throw new RuntimeException('Nie udało się zapisać ticketu Adminera.');
|
||||
}
|
||||
|
||||
$this->alignTicketGroup($path);
|
||||
@chmod($path, 0644);
|
||||
}
|
||||
|
||||
private function alignTicketGroup(string $path): void
|
||||
{
|
||||
$dirGroupId = @filegroup($this->ticketsDir);
|
||||
if (!is_int($dirGroupId) || $dirGroupId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$targetGroup = null;
|
||||
if (function_exists('posix_getgrgid')) {
|
||||
$groupInfo = @posix_getgrgid($dirGroupId);
|
||||
if (is_array($groupInfo) && isset($groupInfo['name']) && is_string($groupInfo['name'])) {
|
||||
$targetGroup = $groupInfo['name'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($targetGroup === null || $targetGroup === '') {
|
||||
$targetGroup = (string)$dirGroupId;
|
||||
}
|
||||
|
||||
@chgrp($path, $targetGroup);
|
||||
}
|
||||
|
||||
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(self::TMP_ROLE_PREFIX) - 1 - strlen($suffix);
|
||||
if ($maxBaseLen < 1) {
|
||||
$maxBaseLen = 1;
|
||||
}
|
||||
$safeBase = substr($base, 0, $maxBaseLen);
|
||||
$role = self::TMP_ROLE_PREFIX . $safeBase . '_' . $suffix;
|
||||
if (!$this->pg->roleExists($role)) {
|
||||
return $role;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('Nie udało się wygenerować unikalnej tymczasowej roli dla Adminera.');
|
||||
}
|
||||
|
||||
private function generateTicketId(): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode(random_bytes(24)), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
private function buildUserBinding(): string
|
||||
{
|
||||
$ua = Http::server('HTTP_USER_AGENT');
|
||||
if ($ua === '') {
|
||||
$raw = getenv('HTTP_USER_AGENT');
|
||||
if ($raw !== false) {
|
||||
$ua = trim((string)$raw);
|
||||
}
|
||||
}
|
||||
return hash('sha256', $ua);
|
||||
}
|
||||
|
||||
private function buildAdminerBaseUrl(): string
|
||||
{
|
||||
$path = $this->settings->adminerUrlPath();
|
||||
$customBase = $this->settings->adminerPublicBaseUrl();
|
||||
if ($customBase !== '') {
|
||||
return $customBase . $path;
|
||||
}
|
||||
|
||||
$host = trim(Http::server('HTTP_HOST'));
|
||||
if ($host === '') {
|
||||
$host = trim(Http::server('SERVER_NAME'));
|
||||
}
|
||||
if ($host === '') {
|
||||
return $path;
|
||||
}
|
||||
|
||||
$host = preg_replace('/:\d+$/', '', $host) ?? $host;
|
||||
$scheme = $this->detectRequestScheme();
|
||||
if ($scheme !== '') {
|
||||
return $scheme . '://' . $host . $path;
|
||||
}
|
||||
|
||||
// Fallback bezpieczny: odziedzicz protokół aktualnej strony (unika mixed content).
|
||||
return '//' . $host . $path;
|
||||
}
|
||||
|
||||
private function detectRequestScheme(): string
|
||||
{
|
||||
$forwardedProto = strtolower(trim(Http::server('HTTP_X_FORWARDED_PROTO')));
|
||||
if ($forwardedProto === 'https' || $forwardedProto === 'http') {
|
||||
return $forwardedProto;
|
||||
}
|
||||
|
||||
$requestScheme = strtolower(trim(Http::server('REQUEST_SCHEME')));
|
||||
if ($requestScheme === 'https' || $requestScheme === 'http') {
|
||||
return $requestScheme;
|
||||
}
|
||||
|
||||
$frontEndHttps = strtolower(trim(Http::server('HTTP_FRONT_END_HTTPS')));
|
||||
if ($frontEndHttps === 'on' || $frontEndHttps === '1' || $frontEndHttps === 'https') {
|
||||
return 'https';
|
||||
}
|
||||
|
||||
$https = strtolower(trim(Http::server('HTTPS')));
|
||||
if ($https === 'on' || $https === '1' || $https === 'https' || $https === 'true') {
|
||||
return 'https';
|
||||
}
|
||||
|
||||
$serverPort = trim(Http::server('SERVER_PORT'));
|
||||
if ($serverPort === '443') {
|
||||
return 'https';
|
||||
}
|
||||
if ($serverPort === '80') {
|
||||
return 'http';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
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,296 @@
|
||||
<?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
|
||||
{
|
||||
return $this->isPluginEnabledByCustomItem() && $this->isDatabaseQuotaEnabled() && $this->isPluginAllowedByPluginRules();
|
||||
}
|
||||
|
||||
public function pluginAccessErrorMessage(): string
|
||||
{
|
||||
if (!$this->isPluginEnabledByCustomItem()) {
|
||||
return 'Plugin PostgreSQL jest wyłączony dla tego konta lub pakietu.';
|
||||
}
|
||||
|
||||
if (!$this->isDatabaseQuotaEnabled()) {
|
||||
return 'Plugin PostgreSQL jest wyłączony: limit baz danych PostgreSQL ustawiono na 0 i nie zaznaczono opcji Bez Ograniczeń.';
|
||||
}
|
||||
|
||||
if (!$this->isPluginAllowedByPluginRules()) {
|
||||
return 'Plugin PostgreSQL jest zablokowany przez reguły plugins_allow/plugins_deny.';
|
||||
}
|
||||
|
||||
return 'Brak dostępu do pluginu PostgreSQL.';
|
||||
}
|
||||
|
||||
public function maxDatabases(): int
|
||||
{
|
||||
if ($this->isUnlimitedDatabasesByCustomItem()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$rawValue = $this->rawDatabasesLimitValue();
|
||||
if ($rawValue === null || trim($rawValue) === '') {
|
||||
$default = $this->settings->defaultDatabasesLimit();
|
||||
return $default < 0 ? 0 : $default;
|
||||
}
|
||||
|
||||
$raw = trim($rawValue);
|
||||
$normalized = strtolower(trim($raw));
|
||||
if ($normalized === 'unlimited') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!preg_match('/^[0-9]+$/', trim($raw))) {
|
||||
$default = $this->settings->defaultDatabasesLimit();
|
||||
return $default < 0 ? 0 : $default;
|
||||
}
|
||||
|
||||
$limit = (int)trim($raw);
|
||||
return $limit < 0 ? 0 : $limit;
|
||||
}
|
||||
|
||||
private function isPluginEnabledByCustomItem(): bool
|
||||
{
|
||||
$raw = $this->readScopedValue('postgresql_enabled');
|
||||
if ($raw === null) {
|
||||
return false;
|
||||
}
|
||||
return Settings::toBool($raw, false);
|
||||
}
|
||||
|
||||
private function isUnlimitedDatabasesByCustomItem(): bool
|
||||
{
|
||||
$unlimitedFlags = [
|
||||
$this->readScopedValue('upostgresql'),
|
||||
$this->readScopedValue('upostgresql_max_databases'),
|
||||
$this->readScopedValue('postgresql_unlimited'), // backward compatibility
|
||||
];
|
||||
|
||||
foreach ($unlimitedFlags as $raw) {
|
||||
if ($raw !== null && Settings::toBool($raw, false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$rawLimit = $this->rawDatabasesLimitValue();
|
||||
if ($rawLimit !== null && strtolower(trim($rawLimit)) === 'unlimited') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isDatabaseQuotaEnabled(): bool
|
||||
{
|
||||
if ($this->isUnlimitedDatabasesByCustomItem()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->maxDatabases() > 0;
|
||||
}
|
||||
|
||||
private function rawDatabasesLimitValue(): ?string
|
||||
{
|
||||
$raw = $this->readScopedValue('postgresql_max_databases');
|
||||
if ($raw === null || trim($raw) === '') {
|
||||
$raw = $this->readScopedValue('postgresql');
|
||||
}
|
||||
return $raw;
|
||||
}
|
||||
|
||||
private function isPluginAllowedByPluginRules(): bool
|
||||
{
|
||||
$pluginId = 'da-postgresql';
|
||||
|
||||
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 $k) {
|
||||
if (!empty($userConf[$k]) && preg_match('/^[A-Za-z0-9._-]+$/', $userConf[$k])) {
|
||||
$owners[] = $userConf[$k];
|
||||
}
|
||||
}
|
||||
$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_POSTGRESQL_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_POSTGRESQL_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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class NamePolicy
|
||||
{
|
||||
public const MAX_IDENTIFIER_LEN = 63;
|
||||
public const MAX_HOST_COMMENT_LEN = 180;
|
||||
public const ALL_IPV4_HOST_PATTERN = '0.0.0.0/0';
|
||||
|
||||
/**
|
||||
* @var array<string,string>
|
||||
*/
|
||||
public const PRIVILEGE_LABELS = [
|
||||
'ALL' => 'Wszystkie uprawnienia',
|
||||
'CONNECT' => 'CONNECT',
|
||||
'CREATE' => 'CREATE (baza/schemat)',
|
||||
'TEMPORARY' => 'TEMPORARY',
|
||||
'SCHEMA_USAGE' => 'USAGE (schema public)',
|
||||
'TABLE_SELECT' => 'SELECT (tabele)',
|
||||
'TABLE_INSERT' => 'INSERT (tabele)',
|
||||
'TABLE_UPDATE' => 'UPDATE (tabele)',
|
||||
'TABLE_DELETE' => 'DELETE (tabele)',
|
||||
'TABLE_TRUNCATE' => 'TRUNCATE (tabele)',
|
||||
'TABLE_REFERENCES' => 'REFERENCES (tabele)',
|
||||
'TABLE_TRIGGER' => 'TRIGGER (tabele)',
|
||||
'SEQUENCE_USAGE' => 'USAGE (sekwencje)',
|
||||
'SEQUENCE_SELECT' => 'SELECT (sekwencje)',
|
||||
'SEQUENCE_UPDATE' => 'UPDATE (sekwencje)',
|
||||
'FUNCTION_EXECUTE' => 'EXECUTE (funkcje)',
|
||||
];
|
||||
|
||||
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 (strpos($value, $prefix) !== 0) {
|
||||
throw new InvalidArgumentException($label . ' musi zaczynać się od prefiksu: ' . $prefix);
|
||||
}
|
||||
|
||||
if (strlen($value) > self::MAX_IDENTIFIER_LEN) {
|
||||
throw new InvalidArgumentException($label . ' przekracza limit 63 znaków PostgreSQL.');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 normalizeRemoteIpv4AccessPattern(string $input): string
|
||||
{
|
||||
$host = trim(strtolower($input));
|
||||
if ($host === '') {
|
||||
throw new InvalidArgumentException('Adres IPv4 albo CIDR jest wymagany.');
|
||||
}
|
||||
|
||||
if (self::isStrictIpv4($host)) {
|
||||
return $host;
|
||||
}
|
||||
|
||||
if (preg_match('#^([^/]+)/([0-9]{1,2})$#', $host, $m)) {
|
||||
$ip = $m[1];
|
||||
$prefix = (int)$m[2];
|
||||
|
||||
if (!self::isStrictIpv4($ip) || $prefix < 0 || $prefix > 32) {
|
||||
throw new InvalidArgumentException('Nieprawidłowy CIDR IPv4: ' . $input);
|
||||
}
|
||||
|
||||
return self::normalizeIpv4Cidr($ip, $prefix);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Dozwolone są pojedyncze adresy IPv4 albo CIDR, np. 203.0.113.10 lub 203.0.113.0/24.');
|
||||
}
|
||||
|
||||
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 === '') {
|
||||
continue;
|
||||
}
|
||||
if (!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;
|
||||
}
|
||||
|
||||
private static function normalizeIpv4Cidr(string $ip, int $prefix): string
|
||||
{
|
||||
$long = ip2long($ip);
|
||||
if ($long === false) {
|
||||
throw new InvalidArgumentException('Nieprawidłowy adres IPv4: ' . $ip);
|
||||
}
|
||||
|
||||
$unsigned = (int)sprintf('%u', $long);
|
||||
$mask = $prefix === 0 ? 0 : ((0xFFFFFFFF << (32 - $prefix)) & 0xFFFFFFFF);
|
||||
$network = $unsigned & $mask;
|
||||
$networkIp = long2ip($network);
|
||||
if ($networkIp === false) {
|
||||
throw new InvalidArgumentException('Nieprawidłowy CIDR IPv4: ' . $ip . '/' . $prefix);
|
||||
}
|
||||
|
||||
return $networkIp . '/' . $prefix;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,365 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
public static function loadPostgresqlCredentials(string $path): array
|
||||
{
|
||||
if (!is_readable($path)) {
|
||||
throw new RuntimeException('Brak pliku z danymi PostgreSQL: ' . $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]) : '5432'),
|
||||
'database' => trim($parts[2]) !== '' ? trim($parts[2]) : 'postgres',
|
||||
'user' => trim($parts[3]),
|
||||
'password' => trim(implode(':', array_slice($parts, 4))),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($kv['PG_HOST']) || !empty($kv['PG_USER']) || !empty($kv['PG_PASSWORD'])) {
|
||||
return [
|
||||
'host' => $kv['PG_HOST'] ?? 'localhost',
|
||||
'port' => (int)($kv['PG_PORT'] ?? '5432'),
|
||||
'database' => $kv['PG_DATABASE'] ?? ($kv['PG_DBNAME'] ?? 'postgres'),
|
||||
'user' => $kv['PG_USER'] ?? 'postgres',
|
||||
'password' => $kv['PG_PASSWORD'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
throw new RuntimeException('Niepoprawny format pliku danych PostgreSQL: ' . $path);
|
||||
}
|
||||
|
||||
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('PG_PLUGIN_DEFAULT_DB_LIMIT', 5);
|
||||
return $limit < 0 ? 0 : $limit;
|
||||
}
|
||||
|
||||
public function maxHostsPerUser(): int
|
||||
{
|
||||
$limit = $this->getInt('PG_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'], true);
|
||||
}
|
||||
|
||||
return $this->getBool('PG_PLUGIN_ALLOW_REMOTE_HOSTS', true);
|
||||
}
|
||||
|
||||
public function hbaSyncScript(): string
|
||||
{
|
||||
$path = $this->getString('PG_PLUGIN_SUDO_SYNC_HBA', '/usr/local/directadmin/plugins/da-postgresql/scripts/setup/pg_hba_sync.sh');
|
||||
return $path !== '' ? $path : '/usr/local/directadmin/plugins/da-postgresql/scripts/setup/pg_hba_sync.sh';
|
||||
}
|
||||
|
||||
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_ADMINER', false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isAdminerInstalled();
|
||||
}
|
||||
|
||||
public function adminerUrlPath(): string
|
||||
{
|
||||
$path = trim($this->getString('ADMINER_URL_PATH', '/adminer'));
|
||||
if ($path === '') {
|
||||
return '/adminer';
|
||||
}
|
||||
|
||||
if ($path[0] !== '/') {
|
||||
$path = '/' . $path;
|
||||
}
|
||||
|
||||
$path = rtrim($path, '/');
|
||||
return $path !== '' ? $path : '/adminer';
|
||||
}
|
||||
|
||||
public function adminerPublicBaseUrl(): string
|
||||
{
|
||||
$url = trim($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('ADMINER_PUBLIC', false);
|
||||
}
|
||||
|
||||
public function adminerSsoDir(): string
|
||||
{
|
||||
$path = trim($this->getString('ADMINER_SSO_DIR', '/var/www/html/adminer/runtime'));
|
||||
if ($path === '') {
|
||||
$path = '/var/www/html/adminer/runtime';
|
||||
}
|
||||
return rtrim($path, '/');
|
||||
}
|
||||
|
||||
public function adminerTicketTtl(): int
|
||||
{
|
||||
$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('ADMINER_SESSION_TTL', 900);
|
||||
if ($ttl < 60) {
|
||||
return 60;
|
||||
}
|
||||
if ($ttl > 3600) {
|
||||
return 3600;
|
||||
}
|
||||
return $ttl;
|
||||
}
|
||||
|
||||
public function adminerRoleTtl(): int
|
||||
{
|
||||
$ttl = $this->getInt('ADMINER_ROLE_TTL', 1200);
|
||||
if ($ttl < 120) {
|
||||
return 120;
|
||||
}
|
||||
if ($ttl > 7200) {
|
||||
return 7200;
|
||||
}
|
||||
return $ttl;
|
||||
}
|
||||
|
||||
private function isAdminerInstalled(): bool
|
||||
{
|
||||
$indexFile = '/var/www/html/adminer/index.php';
|
||||
$coreFile = '/var/www/html/adminer/adminer-core.php';
|
||||
|
||||
return is_file($indexFile) && is_readable($indexFile)
|
||||
&& is_file($coreFile) && is_readable($coreFile);
|
||||
}
|
||||
|
||||
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/postgres_backup');
|
||||
if ($path === '') {
|
||||
$path = '/home/admin/postgres_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/postgres_backup');
|
||||
if ($path === '') {
|
||||
$path = '/home/DA_USER/postgres_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 . '|da-postgresql');
|
||||
}
|
||||
|
||||
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 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