userSession->getUser(); if ($user === null) { throw new OCSPreconditionFailedException('Not logged in'); } 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, bool $notifyEmail = false, bool $enabled = true, ): DataResponse { $this->validate($title, $boardId, $sourceStackId, $targetStackId); $workflow = new Workflow(); $workflow->setUserId($this->currentUserId()); $this->applyFields($workflow, $title, $boardId, $sourceStackId, $targetStackId, $notifyEmail, $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, bool $notifyEmail = false, bool $enabled = true, ): DataResponse { $workflow = $this->findOwnWorkflow($id); $this->validate($title, $boardId, $sourceStackId, $targetStackId); // 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, $notifyEmail, $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 not found'); } 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 not found'); } return $workflow; } /** * @return array */ private function ownedBoards(): array { $userId = $this->currentUserId(); $this->assertDeck(); try { return $this->deckService->listOwnedBoards($userId); } catch (DeckUnavailableException $e) { throw new OCSPreconditionFailedException('Could not load your Deck boards: ' . $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('Not logged in'); } try { $this->deckService->assertDeckAvailable($user); } catch (DeckUnavailableException $e) { throw new OCSPreconditionFailedException('Deck is not available: ' . $e->getMessage()); } } private function validate(string $title, int $boardId, int $sourceStackId, int $targetStackId): void { if (trim($title) === '') { throw new OCSBadRequestException('Title must not be empty'); } if ($sourceStackId === $targetStackId) { throw new OCSBadRequestException('Source and target stack must differ'); } // 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('The selected board does not exist, is archived or is not owned by you'); } $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('The selected stack does not exist on this board'); } } private function applyFields( Workflow $workflow, string $title, int $boardId, int $sourceStackId, int $targetStackId, bool $notifyEmail, bool $enabled, ): void { $workflow->setTitle($title); $workflow->setBoardId($boardId); $workflow->setSourceStackId($sourceStackId); $workflow->setTargetStackId($targetStackId); $workflow->setNotifyEmail($notifyEmail); $workflow->setEnabled($enabled); } }