279 lines
11 KiB
PHP
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);
|
|
}
|
|
}
|