settingRepository->find($key); } public function getValue(string $key): ?string { return $this->settingRepository->getValue($key); } public function getInt(string $key): ?int { $value = $this->settingRepository->getValue($key); if ($value === null || $value === '') { return null; } return (int) $value; } public function set(string $key, ?string $value, ?string $description = null): bool { return $this->settingRepository->set($key, $value, $description); } public function getDefaultTenantId(): ?int { $id = $this->getInt(self::DEFAULT_TENANT_KEY); return $id && $id > 0 ? $id : null; } public function setDefaultTenantId(?int $tenantId, ?string $description = null): bool { $value = $tenantId && $tenantId > 0 ? (string) $tenantId : null; if ($value !== null) { $exists = $this->tenantRepository->find($tenantId); if (!$exists) { return false; } } $desc = $description ?? 'setting.default_tenant'; return $this->settingRepository->set(self::DEFAULT_TENANT_KEY, $value, $desc); } public function getDefaultRoleId(): ?int { $id = $this->getInt(self::DEFAULT_ROLE_KEY); return $id && $id > 0 ? $id : null; } public function setDefaultRoleId(?int $roleId, ?string $description = null): bool { $value = $roleId && $roleId > 0 ? (string) $roleId : null; if ($value !== null) { $exists = $this->roleRepository->find($roleId); if (!$exists) { return false; } } $desc = $description ?? 'setting.default_role'; return $this->settingRepository->set(self::DEFAULT_ROLE_KEY, $value, $desc); } public function getDefaultDepartmentId(): ?int { $id = $this->getInt(self::DEFAULT_DEPARTMENT_KEY); return $id && $id > 0 ? $id : null; } public function setDefaultDepartmentId(?int $departmentId, ?string $description = null): bool { $value = $departmentId && $departmentId > 0 ? (string) $departmentId : null; if ($value !== null) { $exists = $this->departmentRepository->find($departmentId); if (!$exists) { return false; } } $desc = $description ?? 'setting.default_department'; return $this->settingRepository->set(self::DEFAULT_DEPARTMENT_KEY, $value, $desc); } public function getAppTitle(): ?string { $value = $this->settingRepository->getValue(self::APP_TITLE_KEY); $value = $value !== null ? trim($value) : null; return $value !== '' ? $value : null; } public function setAppTitle(?string $title, ?string $description = null): bool { $value = $title !== null ? trim($title) : null; if ($value === '') { $value = null; } $desc = $description ?? 'setting.app_title'; return $this->settingRepository->set(self::APP_TITLE_KEY, $value, $desc); } public function getAppLocale(): ?string { $value = $this->settingRepository->getValue(self::APP_LOCALE_KEY); $value = $value !== null ? trim((string) $value) : ''; return $value !== '' ? $value : null; } public function setAppLocale(?string $locale, ?string $description = null): bool { $value = $locale !== null ? trim((string) $locale) : ''; if ($value === '') { $value = null; } else { $allowed = defined('APP_LOCALES') ? APP_LOCALES : []; if ($allowed && !in_array($value, $allowed, true)) { return false; } } $desc = $description ?? 'setting.app_locale'; return $this->settingRepository->set(self::APP_LOCALE_KEY, $value, $desc); } public function getAppTheme(): ?string { $value = $this->settingRepository->getValue(self::APP_THEME_KEY); $value = $value !== null ? strtolower(trim((string) $value)) : ''; if (!in_array($value, $this->allowedThemes(), true)) { return null; } return $value; } public function setAppTheme(?string $theme, ?string $description = null): bool { $value = $theme !== null ? strtolower(trim((string) $theme)) : ''; if ($value === '' || !in_array($value, $this->allowedThemes(), true)) { $value = null; } $desc = $description ?? 'setting.app_theme'; return $this->settingRepository->set(self::APP_THEME_KEY, $value, $desc); } public function isUserThemeAllowed(): bool { $value = $this->settingRepository->getValue(self::APP_THEME_USER_KEY); if ($value === null || $value === '') { return true; } return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true); } public function setUserThemeAllowed(bool $allowed, ?string $description = null): bool { $value = $allowed ? '1' : '0'; $desc = $description ?? 'setting.app_theme_user'; return $this->settingRepository->set(self::APP_THEME_USER_KEY, $value, $desc); } private function allowedThemes(): array { return array_keys($this->themeConfigService->all()); } public function isRegistrationEnabled(): bool { $value = $this->settingRepository->getValue(self::APP_REGISTRATION_KEY); if ($value === null || $value === '') { return true; } return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true); } public function setRegistrationEnabled(bool $allowed, ?string $description = null): bool { $value = $allowed ? '1' : '0'; $desc = $description ?? 'setting.app_registration'; return $this->settingRepository->set(self::APP_REGISTRATION_KEY, $value, $desc); } public function getApiTokenDefaultTtlDays(): int { $maxDays = $this->getApiTokenMaxTtlDays(); $value = $this->getInt(self::API_TOKEN_DEFAULT_TTL_DAYS_KEY); if ($value !== null && $value >= self::API_TOKEN_TTL_DAYS_MIN && $value <= self::API_TOKEN_TTL_DAYS_HARD_MAX) { return min($value, $maxDays); } return min(self::API_TOKEN_DEFAULT_TTL_DAYS_FALLBACK, $maxDays); } public function setApiTokenDefaultTtlDays(?int $days, ?string $description = null): bool { $value = $days ?? self::API_TOKEN_DEFAULT_TTL_DAYS_FALLBACK; if ($value < self::API_TOKEN_TTL_DAYS_MIN || $value > self::API_TOKEN_TTL_DAYS_HARD_MAX) { return false; } if ($value > $this->getApiTokenMaxTtlDays()) { return false; } $desc = $description ?? 'setting.api_token_default_ttl_days'; return $this->settingRepository->set(self::API_TOKEN_DEFAULT_TTL_DAYS_KEY, (string) $value, $desc); } public function getApiTokenMaxTtlDays(): int { $value = $this->getInt(self::API_TOKEN_MAX_TTL_DAYS_KEY); if ($value !== null && $value >= self::API_TOKEN_TTL_DAYS_MIN && $value <= self::API_TOKEN_TTL_DAYS_HARD_MAX) { return $value; } return self::API_TOKEN_MAX_TTL_DAYS_FALLBACK; } public function setApiTokenMaxTtlDays(?int $days, ?string $description = null): bool { $value = $days ?? self::API_TOKEN_MAX_TTL_DAYS_FALLBACK; if ($value < self::API_TOKEN_TTL_DAYS_MIN || $value > self::API_TOKEN_TTL_DAYS_HARD_MAX) { return false; } $currentDefault = $this->getInt(self::API_TOKEN_DEFAULT_TTL_DAYS_KEY); if ($currentDefault !== null && $currentDefault > $value) { $defaultUpdated = $this->settingRepository->set( self::API_TOKEN_DEFAULT_TTL_DAYS_KEY, (string) $value, 'setting.api_token_default_ttl_days' ); if (!$defaultUpdated) { return false; } } $desc = $description ?? 'setting.api_token_max_ttl_days'; return $this->settingRepository->set(self::API_TOKEN_MAX_TTL_DAYS_KEY, (string) $value, $desc); } public function getUserInactivityDeactivateDays(): int { $value = $this->getInt(self::USER_INACTIVITY_DEACTIVATE_DAYS_KEY); if ($value !== null && $this->isValidInactivityDays($value)) { return $value; } return self::USER_INACTIVITY_DEACTIVATE_DAYS_FALLBACK; } public function setUserInactivityDeactivateDays(?int $days, ?string $description = null): bool { $value = $days ?? self::USER_INACTIVITY_DEACTIVATE_DAYS_FALLBACK; if (!$this->isValidInactivityDays($value)) { return false; } if ($value === 0) { $deleteUpdated = $this->settingRepository->set( self::USER_INACTIVITY_DELETE_DAYS_KEY, '0', 'setting.user_inactivity_delete_days' ); if (!$deleteUpdated) { return false; } } $desc = $description ?? 'setting.user_inactivity_deactivate_days'; return $this->settingRepository->set(self::USER_INACTIVITY_DEACTIVATE_DAYS_KEY, (string) $value, $desc); } public function getUserInactivityDeleteDays(): int { $deactivateDays = $this->getUserInactivityDeactivateDays(); if ($deactivateDays <= 0) { return 0; } $value = $this->getInt(self::USER_INACTIVITY_DELETE_DAYS_KEY); if ($value !== null && $this->isValidInactivityDays($value)) { return $value; } return self::USER_INACTIVITY_DELETE_DAYS_FALLBACK; } public function setUserInactivityDeleteDays(?int $days, ?string $description = null): bool { $value = $days ?? self::USER_INACTIVITY_DELETE_DAYS_FALLBACK; if (!$this->isValidInactivityDays($value)) { return false; } if ($value > 0 && $this->getUserInactivityDeactivateDays() <= 0) { return false; } $desc = $description ?? 'setting.user_inactivity_delete_days'; return $this->settingRepository->set(self::USER_INACTIVITY_DELETE_DAYS_KEY, (string) $value, $desc); } public function getApiCorsAllowedOrigins(): array { $stored = $this->settingRepository->getValue(self::API_CORS_ALLOWED_ORIGINS_KEY); if ($stored === null || trim($stored) === '') { return []; } return $this->parseCorsOrigins($stored, false) ?? []; } public function getApiCorsAllowedOriginsText(): string { $origins = $this->getApiCorsAllowedOrigins(); return implode("\n", $origins); } public function setApiCorsAllowedOrigins(?string $rawOrigins, ?string $description = null): bool { $value = (string) ($rawOrigins ?? ''); if (trim($value) === '') { return $this->settingRepository->set( self::API_CORS_ALLOWED_ORIGINS_KEY, null, $description ?? 'setting.api_cors_allowed_origins' ); } $origins = $this->parseCorsOrigins($value, true); if ($origins === null) { return false; } return $this->settingRepository->set( self::API_CORS_ALLOWED_ORIGINS_KEY, implode("\n", $origins), $description ?? 'setting.api_cors_allowed_origins' ); } private function parseCorsOrigins(string $rawOrigins, bool $strict): ?array { $parts = preg_split('/[\r\n,]+/', $rawOrigins) ?: []; $normalizedOrigins = []; foreach ($parts as $part) { $origin = $this->normalizeCorsOrigin($part); if ($origin === null) { if ($strict && trim((string) $part) !== '') { return null; } continue; } $normalizedOrigins[$origin] = true; } return array_keys($normalizedOrigins); } private function normalizeCorsOrigin(string $origin): ?string { $origin = trim($origin); if ($origin === '') { return null; } // Browser Origin header never contains a trailing slash. $origin = rtrim($origin, '/'); if ($origin === '') { return null; } $parsed = parse_url($origin); if (!is_array($parsed)) { return null; } $scheme = strtolower((string) ($parsed['scheme'] ?? '')); $host = strtolower((string) ($parsed['host'] ?? '')); $port = isset($parsed['port']) ? (int) $parsed['port'] : null; if (!in_array($scheme, ['http', 'https'], true) || $host === '') { return null; } if ( isset($parsed['path']) || isset($parsed['query']) || isset($parsed['fragment']) || isset($parsed['user']) || isset($parsed['pass']) ) { return null; } $defaultPort = ($scheme === 'https') ? 443 : 80; $portSuffix = ($port !== null && $port !== $defaultPort) ? ':' . $port : ''; return $scheme . '://' . $host . $portSuffix; } public function getMicrosoftSharedClientId(): ?string { $value = $this->settingRepository->getValue(self::MICROSOFT_SHARED_CLIENT_ID_KEY); $value = $value !== null ? trim((string) $value) : ''; return $value !== '' ? $value : null; } public function setMicrosoftSharedClientId(?string $clientId, ?string $description = null): bool { $value = $clientId !== null ? trim((string) $clientId) : ''; if ($value === '') { $value = null; } $desc = $description ?? 'setting.microsoft_shared_client_id'; return $this->settingRepository->set(self::MICROSOFT_SHARED_CLIENT_ID_KEY, $value, $desc); } public function getMicrosoftSharedClientSecretEncrypted(): ?string { $value = $this->settingRepository->getValue(self::MICROSOFT_SHARED_CLIENT_SECRET_ENC_KEY); $value = $value !== null ? trim((string) $value) : ''; return $value !== '' ? $value : null; } public function getMicrosoftSharedClientSecret(): ?string { $encrypted = $this->getMicrosoftSharedClientSecretEncrypted(); if ($encrypted === null) { return null; } try { $decrypted = Crypto::decryptString($encrypted); } catch (\Throwable $exception) { return null; } $decrypted = trim($decrypted); return $decrypted !== '' ? $decrypted : null; } public function setMicrosoftSharedClientSecret(?string $secret, ?string $description = null): bool { $value = $secret !== null ? trim((string) $secret) : ''; if ($value === '') { return $this->settingRepository->set( self::MICROSOFT_SHARED_CLIENT_SECRET_ENC_KEY, null, $description ?? 'setting.microsoft_shared_client_secret_enc' ); } $encrypted = Crypto::encryptString($value); $desc = $description ?? 'setting.microsoft_shared_client_secret_enc'; return $this->settingRepository->set(self::MICROSOFT_SHARED_CLIENT_SECRET_ENC_KEY, $encrypted, $desc); } public function getMicrosoftAuthority(): string { $value = $this->settingRepository->getValue(self::MICROSOFT_AUTHORITY_KEY); $value = trim((string) ($value ?? '')); if ($value === '') { return 'https://login.microsoftonline.com/common/v2.0'; } return rtrim($value, '/'); } public function setMicrosoftAuthority(?string $authority, ?string $description = null): bool { $value = trim((string) ($authority ?? '')); if ($value === '') { $value = 'https://login.microsoftonline.com/common/v2.0'; } if (!preg_match('#^https://#i', $value)) { return false; } $desc = $description ?? 'setting.microsoft_authority'; return $this->settingRepository->set(self::MICROSOFT_AUTHORITY_KEY, rtrim($value, '/'), $desc); } public function getAppPrimaryColor(): ?string { $value = $this->settingRepository->getValue(self::APP_PRIMARY_COLOR_KEY); $value = $value !== null ? strtolower(trim((string) $value)) : ''; if ($value === '') { return null; } if (!preg_match('/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i', $value)) { return null; } return $value; } public function setAppPrimaryColor(?string $color, ?string $description = null): bool { $value = $color !== null ? strtolower(trim((string) $color)) : ''; if ($value !== '' && $value[0] !== '#') { if (preg_match('/^[0-9a-f]{3}$/i', $value) || preg_match('/^[0-9a-f]{4}$/i', $value) || preg_match('/^[0-9a-f]{6}$/i', $value) || preg_match('/^[0-9a-f]{8}$/i', $value)) { $value = '#' . $value; } } if ($value === '') { $value = null; } elseif (!preg_match('/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i', $value)) { return false; } $desc = $description ?? 'setting.app_primary_color'; return $this->settingRepository->set(self::APP_PRIMARY_COLOR_KEY, $value, $desc); } public function getSmtpHost(): ?string { $value = $this->settingRepository->getValue(self::SMTP_HOST_KEY); $value = $value !== null ? trim((string) $value) : ''; return $value !== '' ? $value : null; } public function setSmtpHost(?string $host, ?string $description = null): bool { $value = $host !== null ? trim((string) $host) : ''; if ($value === '') { $value = null; } $desc = $description ?? 'setting.smtp_host'; return $this->settingRepository->set(self::SMTP_HOST_KEY, $value, $desc); } public function getSmtpPort(): ?int { $value = $this->settingRepository->getValue(self::SMTP_PORT_KEY); if ($value === null || $value === '') { return null; } $port = (int) $value; return $port > 0 ? $port : null; } public function setSmtpPort(?int $port, ?string $description = null): bool { $value = $port && $port > 0 ? (string) $port : null; $desc = $description ?? 'setting.smtp_port'; return $this->settingRepository->set(self::SMTP_PORT_KEY, $value, $desc); } public function getSmtpUser(): ?string { $value = $this->settingRepository->getValue(self::SMTP_USER_KEY); $value = $value !== null ? trim((string) $value) : ''; return $value !== '' ? $value : null; } public function setSmtpUser(?string $user, ?string $description = null): bool { $value = $user !== null ? trim((string) $user) : ''; if ($value === '') { $value = null; } $desc = $description ?? 'setting.smtp_user'; return $this->settingRepository->set(self::SMTP_USER_KEY, $value, $desc); } public function getSmtpPassword(): ?string { $value = $this->settingRepository->getValue(self::SMTP_PASSWORD_KEY); $value = $value !== null ? (string) $value : ''; return $value !== '' ? $value : null; } public function setSmtpPassword(?string $password, ?string $description = null): bool { $value = $password !== null ? (string) $password : ''; if ($value === '') { $value = null; } $desc = $description ?? 'setting.smtp_password'; return $this->settingRepository->set(self::SMTP_PASSWORD_KEY, $value, $desc); } public function getSmtpSecure(): ?string { $value = $this->settingRepository->getValue(self::SMTP_SECURE_KEY); $value = $value !== null ? strtolower(trim((string) $value)) : ''; if (!in_array($value, ['tls', 'ssl', 'none'], true)) { return null; } return $value === 'none' ? null : $value; } public function setSmtpSecure(?string $secure, ?string $description = null): bool { $value = $secure !== null ? strtolower(trim((string) $secure)) : ''; if ($value === '' || $value === 'none') { $value = null; } elseif (!in_array($value, ['tls', 'ssl'], true)) { return false; } $desc = $description ?? 'setting.smtp_secure'; return $this->settingRepository->set(self::SMTP_SECURE_KEY, $value, $desc); } public function getSmtpFrom(): ?string { $value = $this->settingRepository->getValue(self::SMTP_FROM_KEY); $value = $value !== null ? trim((string) $value) : ''; return $value !== '' ? $value : null; } public function setSmtpFrom(?string $from, ?string $description = null): bool { $value = $from !== null ? trim((string) $from) : ''; if ($value === '') { $value = null; } $desc = $description ?? 'setting.smtp_from'; return $this->settingRepository->set(self::SMTP_FROM_KEY, $value, $desc); } public function getSmtpFromName(): ?string { $value = $this->settingRepository->getValue(self::SMTP_FROM_NAME_KEY); $value = $value !== null ? trim((string) $value) : ''; return $value !== '' ? $value : null; } public function setSmtpFromName(?string $fromName, ?string $description = null): bool { $value = $fromName !== null ? trim((string) $fromName) : ''; if ($value === '') { $value = null; } $desc = $description ?? 'setting.smtp_from_name'; return $this->settingRepository->set(self::SMTP_FROM_NAME_KEY, $value, $desc); } private function isValidInactivityDays(int $days): bool { return $days >= self::USER_INACTIVITY_DAYS_MIN && $days <= self::USER_INACTIVITY_DAYS_MAX; } }