Files

279 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\NuqloudScrum\Service;
use OCA\NuqloudScrum\AppInfo\Application;
use OCP\IConfig;
class ConfigService {
private const KEY_ENABLED = 'nuqloud_scrum_enabled';
private const KEY_AUTOMATION_CREDENTIAL_SOURCE = 'automation_credential_source';
private const KEY_TALK_SERVICE_USER = 'talk_service_user';
private const KEY_TALK_SERVICE_APP_PASSWORD = 'talk_service_app_password';
private const KEY_WORKSPACE_FILTER_MODE = 'workspace_filter_mode';
private const KEY_WORKSPACE_ALLOWLIST = 'workspace_allowlist';
private const KEY_DECK_SYNC_ENABLED = 'deck_sync_enabled';
private const KEY_FILE_MIRROR_ENABLED = 'file_mirror_enabled';
private const KEY_FILE_MIRROR_MODE = 'file_mirror_mode';
private const KEY_REACTION_WORKFLOW_ENABLED = 'reaction_workflow_enabled';
private const KEY_BOT_COMMANDS_ENABLED = 'bot_commands_enabled';
private const KEY_SUMMARY_ENABLED = 'summary_enabled';
private const KEY_SYNC_INTERVAL_MINUTES = 'sync_interval_minutes';
public function __construct(private IConfig $config) {
}
/** @return array<string, mixed> */
public function getPublicSettings(): array {
$internal = $this->getInternalSettings();
unset($internal['talkServiceAppPassword']);
$internal['talkServiceConfigured'] = $this->hasResolvedServiceCredentials();
$internal['manualTalkServiceConfigured'] = $this->hasManualServiceCredentials();
$internal['automationAccountSources'] = $this->getAutomationAccountSources();
return $internal;
}
/** @return array<string, mixed> */
public function getInternalSettings(): array {
return [
'enabled' => $this->getBool(self::KEY_ENABLED, false),
'automationCredentialSource' => $this->getEnum(self::KEY_AUTOMATION_CREDENTIAL_SOURCE, 'manual', ['manual', 'env_admin', 'env_service']),
'talkServiceUser' => $this->getString(self::KEY_TALK_SERVICE_USER, ''),
'talkServiceAppPassword' => $this->getString(self::KEY_TALK_SERVICE_APP_PASSWORD, ''),
'workspaceFilterMode' => $this->getEnum(self::KEY_WORKSPACE_FILTER_MODE, 'disabled', ['all', 'allowlist', 'selected', 'disabled']),
'workspaceAllowlist' => $this->getAllowlist(),
'deckSyncEnabled' => $this->getBool(self::KEY_DECK_SYNC_ENABLED, true),
'fileMirrorEnabled' => $this->getBool(self::KEY_FILE_MIRROR_ENABLED, true),
'fileMirrorMode' => $this->getEnum(self::KEY_FILE_MIRROR_MODE, 'copy', ['copy', 'link']),
'reactionWorkflowEnabled' => $this->getBool(self::KEY_REACTION_WORKFLOW_ENABLED, true),
'botCommandsEnabled' => $this->getBool(self::KEY_BOT_COMMANDS_ENABLED, false),
'summaryEnabled' => $this->getBool(self::KEY_SUMMARY_ENABLED, false),
'syncIntervalMinutes' => $this->getInt(self::KEY_SYNC_INTERVAL_MINUTES, 5, 1, 1440),
];
}
/** @param array<string, mixed> $payload */
/** @return array<string, mixed> */
public function saveFromArray(array $payload): array {
$current = $this->getInternalSettings();
$enabled = $this->coerceBool($payload['enabled'] ?? $current['enabled'], (bool)$current['enabled']);
$deckSyncEnabled = $this->coerceBool($payload['deckSyncEnabled'] ?? $current['deckSyncEnabled'], (bool)$current['deckSyncEnabled']);
$fileMirrorEnabled = $this->coerceBool($payload['fileMirrorEnabled'] ?? $current['fileMirrorEnabled'], (bool)$current['fileMirrorEnabled']);
$reactionWorkflowEnabled = $this->coerceBool($payload['reactionWorkflowEnabled'] ?? $current['reactionWorkflowEnabled'], (bool)$current['reactionWorkflowEnabled']);
$botCommandsEnabled = $this->coerceBool($payload['botCommandsEnabled'] ?? $current['botCommandsEnabled'], (bool)$current['botCommandsEnabled']);
$summaryEnabled = $this->coerceBool($payload['summaryEnabled'] ?? $current['summaryEnabled'], (bool)$current['summaryEnabled']);
$automationCredentialSource = $this->sanitizeEnum((string)($payload['automationCredentialSource'] ?? $current['automationCredentialSource']), ['manual', 'env_admin', 'env_service'], 'manual');
$talkServiceUser = $this->limitString((string)($payload['talkServiceUser'] ?? $current['talkServiceUser']), 256);
$workspaceFilterMode = $this->sanitizeEnum((string)($payload['workspaceFilterMode'] ?? $current['workspaceFilterMode']), ['all', 'allowlist', 'selected', 'disabled'], 'disabled');
$fileMirrorMode = $this->sanitizeEnum((string)($payload['fileMirrorMode'] ?? $current['fileMirrorMode']), ['copy', 'link'], 'copy');
$workspaceAllowlist = $this->sanitizeAllowlist($payload['workspaceAllowlist'] ?? $current['workspaceAllowlist']);
$syncIntervalMinutes = $this->coerceInt($payload['syncIntervalMinutes'] ?? $current['syncIntervalMinutes'], 5, 1, 1440);
$talkServiceAppPassword = (string)$current['talkServiceAppPassword'];
if (array_key_exists('talkServiceAppPassword', $payload)) {
$newPassword = trim((string)$payload['talkServiceAppPassword']);
if ($newPassword !== '') {
$talkServiceAppPassword = $newPassword;
}
}
if (!empty($payload['clearTalkServiceAppPassword'])) {
$talkServiceAppPassword = '';
}
$this->setBool(self::KEY_ENABLED, $enabled);
$this->setString(self::KEY_AUTOMATION_CREDENTIAL_SOURCE, $automationCredentialSource);
$this->setString(self::KEY_TALK_SERVICE_USER, $talkServiceUser);
$this->setString(self::KEY_TALK_SERVICE_APP_PASSWORD, $talkServiceAppPassword);
$this->setString(self::KEY_WORKSPACE_FILTER_MODE, $workspaceFilterMode);
$this->setJson(self::KEY_WORKSPACE_ALLOWLIST, $workspaceAllowlist);
$this->setBool(self::KEY_DECK_SYNC_ENABLED, $deckSyncEnabled);
$this->setBool(self::KEY_FILE_MIRROR_ENABLED, $fileMirrorEnabled);
$this->setString(self::KEY_FILE_MIRROR_MODE, $fileMirrorMode);
$this->setBool(self::KEY_REACTION_WORKFLOW_ENABLED, $reactionWorkflowEnabled);
$this->setBool(self::KEY_BOT_COMMANDS_ENABLED, $botCommandsEnabled);
$this->setBool(self::KEY_SUMMARY_ENABLED, $summaryEnabled);
$this->setString(self::KEY_SYNC_INTERVAL_MINUTES, (string)$syncIntervalMinutes);
return $this->getPublicSettings();
}
public function isRunnable(): bool {
$settings = $this->getInternalSettings();
return (bool)$settings['enabled']
&& $this->hasResolvedServiceCredentials()
&& (string)$settings['workspaceFilterMode'] !== 'disabled';
}
/** @return array{user: string, password: string, source: string} */
public function getAutomationCredentials(): array {
$settings = $this->getInternalSettings();
$source = (string)$settings['automationCredentialSource'];
if ($source === 'env_admin') {
return [
'user' => $this->getEnvString('NEXTCLOUD_ADMIN_USER'),
'password' => $this->getEnvString('NEXTCLOUD_ADMIN_PASSWORD'),
'source' => $source,
];
}
if ($source === 'env_service') {
return [
'user' => $this->getEnvString('NEXTCLOUD_SERVICE_USER'),
'password' => $this->getEnvString('NEXTCLOUD_SERVICE_PASSWORD'),
'source' => $source,
];
}
return [
'user' => trim((string)$settings['talkServiceUser']),
'password' => trim((string)$settings['talkServiceAppPassword']),
'source' => 'manual',
];
}
public function getAutomationUserId(): string {
return $this->getAutomationCredentials()['user'];
}
private function hasResolvedServiceCredentials(): bool {
$credentials = $this->getAutomationCredentials();
return $credentials['user'] !== '' && $credentials['password'] !== '';
}
private function hasManualServiceCredentials(): bool {
return $this->getString(self::KEY_TALK_SERVICE_USER, '') !== ''
&& $this->getString(self::KEY_TALK_SERVICE_APP_PASSWORD, '') !== '';
}
/** @return array<string, array<string, mixed>> */
private function getAutomationAccountSources(): array {
$adminUser = $this->getEnvString('NEXTCLOUD_ADMIN_USER');
$adminPassword = $this->getEnvString('NEXTCLOUD_ADMIN_PASSWORD');
$serviceUser = $this->getEnvString('NEXTCLOUD_SERVICE_USER');
$servicePassword = $this->getEnvString('NEXTCLOUD_SERVICE_PASSWORD');
return [
'manual' => [
'user' => $this->getString(self::KEY_TALK_SERVICE_USER, ''),
'configured' => $this->hasManualServiceCredentials(),
],
'env_admin' => [
'user' => $adminUser,
'configured' => $adminUser !== '' && $adminPassword !== '',
],
'env_service' => [
'user' => $serviceUser,
'configured' => $serviceUser !== '' && $servicePassword !== '',
],
];
}
private function getEnvString(string $key): string {
$value = getenv($key);
if ($value === false && isset($_SERVER[$key])) {
$value = $_SERVER[$key];
}
if ($value === false && isset($_ENV[$key])) {
$value = $_ENV[$key];
}
return trim((string)($value === false ? '' : $value));
}
private function getString(string $key, string $default): string {
return trim((string)$this->config->getAppValue(Application::APP_ID, $key, $default));
}
private function setString(string $key, string $value): void {
$this->config->setAppValue(Application::APP_ID, $key, $value);
}
private function getBool(string $key, bool $default): bool {
return $this->config->getAppValue(Application::APP_ID, $key, $default ? '1' : '0') === '1';
}
private function setBool(string $key, bool $value): void {
$this->config->setAppValue(Application::APP_ID, $key, $value ? '1' : '0');
}
/** @param array<int, mixed> $allowed */
private function getEnum(string $key, string $default, array $allowed): string {
return $this->sanitizeEnum($this->getString($key, $default), $allowed, $default);
}
/** @param array<int, mixed> $allowed */
private function sanitizeEnum(string $value, array $allowed, string $default): string {
$value = strtolower(trim($value));
return in_array($value, $allowed, true) ? $value : $default;
}
private function getInt(string $key, int $default, int $min, int $max): int {
return $this->coerceInt($this->getString($key, (string)$default), $default, $min, $max);
}
private function coerceInt(mixed $value, int $default, int $min, int $max): int {
$int = filter_var($value, FILTER_VALIDATE_INT);
if ($int === false) {
$int = $default;
}
return max($min, min($max, (int)$int));
}
private function coerceBool(mixed $value, bool $default): bool {
$bool = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
return $bool ?? $default;
}
/** @return array<int, string> */
private function getAllowlist(): array {
$raw = $this->config->getAppValue(Application::APP_ID, self::KEY_WORKSPACE_ALLOWLIST, '[]');
try {
$decoded = json_decode((string)$raw, true, 512, JSON_THROW_ON_ERROR);
} catch (\Throwable) {
return [];
}
return $this->sanitizeAllowlist($decoded);
}
/** @return array<int, string> */
private function sanitizeAllowlist(mixed $value): array {
if (is_string($value)) {
$value = preg_split('/[\r\n,]+/', $value) ?: [];
}
if (!is_array($value)) {
return [];
}
$items = [];
foreach ($value as $entry) {
$item = $this->limitString((string)$entry, 255);
if ($item === '') {
continue;
}
$items[$item] = $item;
}
return array_values($items);
}
/** @param array<int, string> $value */
private function setJson(string $key, array $value): void {
$this->config->setAppValue(
Application::APP_ID,
$key,
json_encode($value, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)
);
}
private function limitString(string $value, int $maxLength): string {
$value = trim($value);
if (function_exists('mb_substr')) {
return mb_substr($value, 0, $maxLength);
}
return substr($value, 0, $maxLength);
}
}