Build package / xml-lint (push) Successful in 33s
Build package / php-lint (8.5) (push) Successful in 1m48s
Build package / php-lint (8.4) (push) Successful in 1m50s
Build package / unit-tests (push) Successful in 1m52s
Build package / package (push) Successful in 1m38s
Each workflow now has a notify channel (mail, notification or both) next to its recipient mode. Notifications go out per card through OCP\Notification\IManager, so they show up in the bell and as push on the mobile and desktop clients. Without the notifications app nothing is sent. The one-off "workflow disabled" notice is sent as a notification as well. Existing workflows keep mail (migration Version1004). Bumps to 35.3.0 so the migration runs.
223 lines
7.4 KiB
PHP
223 lines
7.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace OCA\WorkflowDeckAutomation\Controller;
|
|
|
|
use OCA\WorkflowDeckAutomation\Db\Workflow;
|
|
use OCA\WorkflowDeckAutomation\Db\WorkflowMapper;
|
|
use OCA\WorkflowDeckAutomation\Service\DeckIntegrationService;
|
|
use OCA\WorkflowDeckAutomation\Service\DeckUnavailableException;
|
|
use OCA\WorkflowDeckAutomation\Service\NotificationPolicy;
|
|
use OCP\AppFramework\Db\DoesNotExistException;
|
|
use OCP\AppFramework\Http;
|
|
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
|
use OCP\AppFramework\Http\DataResponse;
|
|
use OCP\AppFramework\OCS\OCSBadRequestException;
|
|
use OCP\AppFramework\OCS\OCSNotFoundException;
|
|
use OCP\AppFramework\OCS\OCSPreconditionFailedException;
|
|
use OCP\AppFramework\OCSController;
|
|
use OCP\IRequest;
|
|
use OCP\IUserSession;
|
|
|
|
class WorkflowController extends OCSController {
|
|
public function __construct(
|
|
string $appName,
|
|
IRequest $request,
|
|
private WorkflowMapper $workflowMapper,
|
|
private DeckIntegrationService $deckService,
|
|
private IUserSession $userSession,
|
|
) {
|
|
parent::__construct($appName, $request);
|
|
}
|
|
|
|
private function currentUserId(): string {
|
|
$user = $this->userSession->getUser();
|
|
if ($user === null) {
|
|
throw new OCSPreconditionFailedException('Nicht angemeldet');
|
|
}
|
|
return $user->getUID();
|
|
}
|
|
|
|
/**
|
|
* Every workflow on a board the user owns, no matter who created it: a
|
|
* workflow belongs to its board's owner, and follows the board when
|
|
* Deck transfers it.
|
|
*/
|
|
#[NoAdminRequired]
|
|
public function index(): DataResponse {
|
|
$workflows = $this->workflowMapper->findAllForBoards($this->ownedBoardIds());
|
|
return new DataResponse(array_map(static fn (Workflow $w) => $w->jsonSerialize(), $workflows));
|
|
}
|
|
|
|
#[NoAdminRequired]
|
|
public function create(
|
|
string $title,
|
|
int $boardId,
|
|
int $sourceStackId,
|
|
int $targetStackId,
|
|
string $notifyMode = NotificationPolicy::MODE_OFF,
|
|
string $notifyChannel = NotificationPolicy::CHANNEL_MAIL,
|
|
bool $enabled = true,
|
|
): DataResponse {
|
|
$this->validate($title, $boardId, $sourceStackId, $targetStackId, $notifyMode, $notifyChannel);
|
|
|
|
$workflow = new Workflow();
|
|
$workflow->setUserId($this->currentUserId());
|
|
$this->applyFields($workflow, $title, $boardId, $sourceStackId, $targetStackId, $notifyMode, $notifyChannel, $enabled);
|
|
|
|
$workflow = $this->workflowMapper->insert($workflow);
|
|
return new DataResponse($workflow->jsonSerialize(), Http::STATUS_CREATED);
|
|
}
|
|
|
|
#[NoAdminRequired]
|
|
public function update(
|
|
int $id,
|
|
string $title,
|
|
int $boardId,
|
|
int $sourceStackId,
|
|
int $targetStackId,
|
|
string $notifyMode = NotificationPolicy::MODE_OFF,
|
|
string $notifyChannel = NotificationPolicy::CHANNEL_MAIL,
|
|
bool $enabled = true,
|
|
): DataResponse {
|
|
$workflow = $this->findOwnWorkflow($id);
|
|
$this->validate($title, $boardId, $sourceStackId, $targetStackId, $notifyMode, $notifyChannel);
|
|
|
|
// Kept in step with the board owner so the job runs as, and mails,
|
|
// the right user; validate() made sure the editor owns the board.
|
|
$workflow->setUserId($this->currentUserId());
|
|
$this->applyFields($workflow, $title, $boardId, $sourceStackId, $targetStackId, $notifyMode, $notifyChannel, $enabled);
|
|
$workflow = $this->workflowMapper->update($workflow);
|
|
|
|
return new DataResponse($workflow->jsonSerialize());
|
|
}
|
|
|
|
#[NoAdminRequired]
|
|
public function destroy(int $id): DataResponse {
|
|
$workflow = $this->findOwnWorkflow($id);
|
|
$this->workflowMapper->delete($workflow);
|
|
return new DataResponse([]);
|
|
}
|
|
|
|
#[NoAdminRequired]
|
|
public function boards(): DataResponse {
|
|
$this->assertDeck();
|
|
return new DataResponse($this->ownedBoards());
|
|
}
|
|
|
|
#[NoAdminRequired]
|
|
public function stacks(int $boardId): DataResponse {
|
|
$this->assertDeck();
|
|
return new DataResponse($this->deckService->listStacks($boardId));
|
|
}
|
|
|
|
private function findOwnWorkflow(int $id): Workflow {
|
|
try {
|
|
$workflow = $this->workflowMapper->findById($id);
|
|
} catch (DoesNotExistException $e) {
|
|
throw new OCSNotFoundException('Workflow nicht gefunden');
|
|
}
|
|
|
|
if (!in_array($workflow->getBoardId(), $this->ownedBoardIds(), true)) {
|
|
// Same answer as for a missing id: don't reveal other users' workflows.
|
|
throw new OCSNotFoundException('Workflow nicht gefunden');
|
|
}
|
|
return $workflow;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array{id: int, title: string, archived: bool}>
|
|
*/
|
|
private function ownedBoards(): array {
|
|
$userId = $this->currentUserId();
|
|
$this->assertDeck();
|
|
try {
|
|
return $this->deckService->listOwnedBoards($userId);
|
|
} catch (DeckUnavailableException $e) {
|
|
throw new OCSPreconditionFailedException('Deine Deck-Boards konnten nicht geladen werden: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return int[]
|
|
*/
|
|
private function ownedBoardIds(): array {
|
|
return array_map(static fn (array $board) => $board['id'], $this->ownedBoards());
|
|
}
|
|
|
|
private function assertDeck(): void {
|
|
$user = $this->userSession->getUser();
|
|
if ($user === null) {
|
|
throw new OCSPreconditionFailedException('Nicht angemeldet');
|
|
}
|
|
try {
|
|
$this->deckService->assertDeckAvailable($user);
|
|
} catch (DeckUnavailableException $e) {
|
|
throw new OCSPreconditionFailedException('Deck ist nicht verfügbar: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function validate(string $title, int $boardId, int $sourceStackId, int $targetStackId, string $notifyMode, string $notifyChannel): void {
|
|
if (trim($title) === '') {
|
|
throw new OCSBadRequestException('Bitte einen Titel angeben.');
|
|
}
|
|
if ($sourceStackId === $targetStackId) {
|
|
throw new OCSBadRequestException('Quell- und Ziel-Stapel müssen sich unterscheiden.');
|
|
}
|
|
if (!NotificationPolicy::isValidMode($notifyMode)) {
|
|
throw new OCSBadRequestException('Ungültige Benachrichtigungs-Einstellung, erlaubt sind: ' . implode(', ', NotificationPolicy::MODES));
|
|
}
|
|
if (!NotificationPolicy::isValidChannel($notifyChannel)) {
|
|
throw new OCSBadRequestException('Ungültiger Benachrichtigungsweg, erlaubt sind: ' . implode(', ', NotificationPolicy::CHANNELS));
|
|
}
|
|
|
|
// Reject references to boards or stacks that are gone (or were never
|
|
// the user's) instead of storing a workflow that can only fail later.
|
|
// assertDeck() ran first, so an empty list here means "not there",
|
|
// not "Deck is unreachable".
|
|
$this->assertDeck();
|
|
|
|
// Only boards the user owns are listed, so this also enforces the
|
|
// owner rule. Archived boards stay listed (their workflows must stay
|
|
// visible) but are no target for a new or changed workflow.
|
|
$board = null;
|
|
foreach ($this->ownedBoards() as $candidate) {
|
|
if ($candidate['id'] === $boardId) {
|
|
$board = $candidate;
|
|
break;
|
|
}
|
|
}
|
|
if ($board === null || $board['archived']) {
|
|
throw new OCSBadRequestException('Das gewählte Board existiert nicht, ist archiviert oder gehört dir nicht.');
|
|
}
|
|
|
|
$stackIds = array_map(
|
|
static fn (array $stack) => (int)$stack['id'],
|
|
$this->deckService->listStacks($boardId),
|
|
);
|
|
if (!in_array($sourceStackId, $stackIds, true) || !in_array($targetStackId, $stackIds, true)) {
|
|
throw new OCSBadRequestException('Der gewählte Stapel existiert auf diesem Board nicht.');
|
|
}
|
|
}
|
|
|
|
private function applyFields(
|
|
Workflow $workflow,
|
|
string $title,
|
|
int $boardId,
|
|
int $sourceStackId,
|
|
int $targetStackId,
|
|
string $notifyMode,
|
|
string $notifyChannel,
|
|
bool $enabled,
|
|
): void {
|
|
$workflow->setTitle($title);
|
|
$workflow->setBoardId($boardId);
|
|
$workflow->setSourceStackId($sourceStackId);
|
|
$workflow->setTargetStackId($targetStackId);
|
|
$workflow->setNotifyMode($notifyMode);
|
|
$workflow->setNotifyChannel($notifyChannel);
|
|
$workflow->setEnabled($enabled);
|
|
}
|
|
}
|