Skip to content

Commit 814fbba

Browse files
feat: implement remote attachment
Signed-off-by: Luka Trovic <luka@nextcloud.com>
1 parent 4ddc0bc commit 814fbba

11 files changed

Lines changed: 543 additions & 1221 deletions

File tree

‎appinfo/routes.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,15 @@
7878

7979
// api
8080
['name' => 'board_api#index', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'GET'],
81-
['name' => 'board_api#get', 'url' => '/api/v{apiVersion}/boards/{boardId}', 'verb' => 'GET'],
8281
['name' => 'board_api#create', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'POST'],
82+
['name' => 'board_api#get', 'url' => '/api/v{apiVersion}/boards/{boardId}', 'verb' => 'GET'],
8383
['name' => 'board_api#delete', 'url' => '/api/v{apiVersion}/boards/{boardId}', 'verb' => 'DELETE'],
8484
['name' => 'board_api#update', 'url' => '/api/v{apiVersion}/boards/{boardId}', 'verb' => 'PUT'],
8585
['name' => 'board_api#undo_delete', 'url' => '/api/v{apiVersion}/boards/{boardId}/undo_delete', 'verb' => 'POST'],
8686
['name' => 'board_api#addAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl', 'verb' => 'POST'],
8787
['name' => 'board_api#deleteAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'],
8888
['name' => 'board_api#updateAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'],
89+
['name' => 'board_api#clone', 'url' => '/api/v{apiVersion}/boards/{boardId}/clone', 'verb' => 'POST'],
8990

9091
['name' => 'board_import_api#getAllowedSystems', 'url' => '/api/v{apiVersion}/boards/import/getSystems','verb' => 'GET'],
9192
['name' => 'board_import_api#getConfigSchema', 'url' => '/api/v{apiVersion}/boards/import/config/schema/{name}','verb' => 'GET'],
@@ -141,6 +142,7 @@
141142
['name' => 'stack_ocs#index', 'url' => '/api/v{apiVersion}/stacks/{boardId}', 'verb' => 'GET'],
142143
['name' => 'stack_ocs#getArchived', 'url' => '/api/v{apiVersion}/stacks/{boardId}/archived', 'verb' => 'GET'],
143144
['name' => 'board_ocs#create', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'POST'],
145+
['name' => 'board_ocs#createForTeam', 'url' => '/api/v{apiVersion}/boards/team', 'verb' => 'POST'],
144146
['name' => 'board_ocs#update', 'url' => '/api/v{apiVersion}/boards/{boardId}', 'verb' => 'PUT'],
145147
['name' => 'board_ocs#addAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl', 'verb' => 'POST'],
146148
['name' => 'board_ocs#leave', 'url' => '/api/v{apiVersion}/boards/{boardId}/leave', 'verb' => 'POST'],

‎lib/Controller/CardOcsController.php‎

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
use OCP\IRequest;
2222

2323
class CardOcsController extends OCSController {
24+
/**
25+
* Sentinel order used when the client does not request a specific position.
26+
* Cards created with this order are appended to the end of the stack.
27+
*/
28+
private const DEFAULT_ORDER = 999;
29+
2430
public function __construct(
2531
string $appName,
2632
IRequest $request,
@@ -36,7 +42,7 @@ public function __construct(
3642

3743
#[NoAdminRequired]
3844
#[PublicPage]
39-
public function create(string $title, int $stackId, ?int $boardId = null, ?string $type = 'plain', ?string $owner = null, ?int $order = 999, ?string $description = '', $duedate = null, $startdate = null, ?array $labels = [], ?array $users = [], ?string $color = null) {
45+
public function create(string $title, int $stackId, ?int $boardId = null, ?string $type = 'plain', ?string $owner = null, ?int $order = self::DEFAULT_ORDER, ?string $description = '', $duedate = null, $startdate = null, ?array $labels = [], ?array $users = [], ?string $color = null) {
4046
if ($boardId) {
4147
$board = $this->boardService->find($boardId, false);
4248
if ($board->getExternalId()) {
@@ -48,7 +54,8 @@ public function create(string $title, int $stackId, ?int $boardId = null, ?strin
4854
if (!$owner) {
4955
$owner = $this->userId;
5056
}
51-
$card = $this->cardService->create($title, $stackId, $type, $order, $owner, $description, $duedate, $startdate, $color);
57+
// An explicit order means the client wants the card at that position, so shift the surrounding cards
58+
$card = $this->cardService->create($title, $stackId, $type, $order, $owner, $description, $duedate, $startdate, $color, insertAtPosition: $order !== self::DEFAULT_ORDER);
5259

5360
foreach ($labels ?? [] as $label) {
5461
$this->assignLabel($boardId, $card->getId(), $label);
@@ -128,24 +135,26 @@ public function update(int $id, string $title, int $stackId, string $type, int $
128135
}
129136
}
130137

131-
$localBoard = $this->boardService->find($boardId, false);
132-
if ($localBoard->getExternalId()) {
133-
return new DataResponse($this->externalBoardService->updateCardOnRemote(
134-
$localBoard,
135-
$id,
136-
$title,
137-
$stackId,
138-
$type,
139-
$owner,
140-
$description,
141-
$order,
142-
$duedate,
143-
$deletedAt,
144-
$archived,
145-
$done,
146-
$startdate,
147-
$color,
148-
));
138+
if ($boardId) {
139+
$localBoard = $this->boardService->find($boardId, false);
140+
if ($localBoard->getExternalId()) {
141+
return new DataResponse($this->externalBoardService->updateCardOnRemote(
142+
$localBoard,
143+
$id,
144+
$title,
145+
$stackId,
146+
$type,
147+
$owner,
148+
$description,
149+
$order,
150+
$duedate,
151+
$deletedAt,
152+
$archived,
153+
$done,
154+
$startdate,
155+
$color,
156+
));
157+
}
149158
}
150159

151160
return new DataResponse($this->cardService->update($id,

‎lib/Service/ExternalBoardService.php‎

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use OCA\Deck\Db\User;
1717
use OCA\Deck\Federation\DeckFederationProxy;
1818
use OCA\Deck\Model\OptionalNullableValue;
19+
use OCA\Deck\Sharing\DeckShareProvider;
1920
use OCP\AppFramework\Http\DataResponse;
2021
use OCP\Federation\ICloudIdManager;
2122
use OCP\IURLGenerator;
@@ -27,10 +28,10 @@ public function __construct(
2728
private ICloudIdManager $cloudIdManager,
2829
private DeckFederationProxy $proxy,
2930
private ConfigService $configService,
30-
private BoardService $boardService,
3131
private PermissionService $permissionService,
3232
private BoardMapper $boardMapper,
3333
private IURLGenerator $urlGenerator,
34+
private DeckShareProvider $deckShareProvider,
3435
private ?string $userId,
3536
) {
3637
}
@@ -586,4 +587,155 @@ public function leaveBoardOnRemote(Board $localBoard): void {
586587
$url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/boards/' . $localBoard->getExternalId() . '/leave';
587588
$this->proxy->post($participantCloudId->getId(), $shareToken, $url);
588589
}
590+
591+
public function localizeRemoteAttachments(Board $localBoard, array $attachments): array {
592+
$ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner());
593+
foreach ($attachments as $i => $attachment) {
594+
$createdByUser = $this->localizeRemoteUser($localBoard, ['uid' => $attachment['createdBy']]);
595+
if ($createdByUser instanceof FederatedUser) {
596+
$attachments[$i]['createdBy'] = $createdByUser->getCloudId()->getId();
597+
$attachments[$i]['createdByRemote'] = $createdByUser->getCloudId()->getRemote();
598+
$attachments[$i]['extendedData']['attachmentCreator']['id'] = $createdByUser->getCloudId()->getId();
599+
$attachments[$i]['extendedData']['attachmentCreator']['remote'] = $createdByUser->getCloudId()->getRemote();
600+
$attachments[$i]['extendedData']['attachmentCreator']['displayName'] = $createdByUser->getCloudId()->getId();
601+
}
602+
if (!empty($attachment['extendedData']['shareToken'])) {
603+
$file = $this->deckShareProvider->ensureAcceptRemoteShare($attachment['extendedData']['shareToken'], $ownerCloudId->getRemote());
604+
605+
if (empty($file)) {
606+
unset($attachments[$i]);
607+
continue;
608+
}
609+
610+
$attachments[$i]['extendedData'] = array_merge($attachments[$i]['extendedData'], $file);
611+
unset($attachments[$i]['extendedData']['shareToken']);
612+
}
613+
}
614+
return $attachments;
615+
}
616+
617+
public function getAttachmentsFromRemote(Board $localBoard, int $cardId): array {
618+
$this->configService->ensureFederationEnabled();
619+
$shareToken = $localBoard->getShareToken();
620+
$participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null);
621+
$ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner());
622+
$url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/attachments';
623+
$resp = $this->proxy->get($participantCloudId->getId(), $shareToken, $url, [
624+
'boardId' => $localBoard->getExternalId(),
625+
]);
626+
$attachments = $this->proxy->getOcsData($resp);
627+
return $this->localizeRemoteAttachments($localBoard, $attachments);
628+
}
629+
630+
public function createAttachmentOnRemote(Board $localBoard, int $cardId, string $type, string $data = '', ?array $uploadedFile = null): array {
631+
$this->configService->ensureFederationEnabled();
632+
$this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false);
633+
$shareToken = $localBoard->getShareToken();
634+
$participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null);
635+
$ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner());
636+
$url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/attachment';
637+
if ($uploadedFile !== null) {
638+
$content = fopen($uploadedFile['tmp_name'], 'rb');
639+
if ($content === false) {
640+
throw new Exception('Could not read uploaded file');
641+
}
642+
$params = [
643+
[
644+
'name' => 'boardId',
645+
'contents' => (string)$localBoard->getExternalId(),
646+
],
647+
[
648+
'name' => 'file',
649+
'contents' => $content,
650+
'filename' => $uploadedFile['name'],
651+
],
652+
[
653+
'name' => 'type',
654+
'contents' => $type,
655+
],
656+
[
657+
'name' => 'data',
658+
'contents' => $data,
659+
],
660+
];
661+
$resp = $this->proxy->post($participantCloudId->getId(), $shareToken, $url, ['multipart' => $params]);
662+
return $this->proxy->getOcsData($resp);
663+
}
664+
$resp = $this->proxy->post($participantCloudId->getId(), $shareToken, $url, [
665+
'boardId' => $localBoard->getExternalId(),
666+
'type' => $type,
667+
'data' => $data,
668+
]);
669+
return $this->proxy->getOcsData($resp);
670+
}
671+
672+
public function updateAttachmentOnRemote(Board $localBoard, int $cardId, int $attachmentId, string $data, string $type = 'file', ?array $uploadedFile = null): array {
673+
$this->configService->ensureFederationEnabled();
674+
$this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false);
675+
$shareToken = $localBoard->getShareToken();
676+
$participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null);
677+
$ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner());
678+
$url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/attachments/' . $attachmentId;
679+
if ($uploadedFile !== null) {
680+
$content = fopen($uploadedFile['tmp_name'], 'rb');
681+
if ($content === false) {
682+
throw new Exception('Could not read uploaded file');
683+
}
684+
$params = [
685+
[
686+
'name' => 'boardId',
687+
'contents' => (string)$localBoard->getExternalId(),
688+
],
689+
[
690+
'name' => 'file',
691+
'contents' => $content,
692+
'filename' => $uploadedFile['name'],
693+
],
694+
[
695+
'name' => 'type',
696+
'contents' => $type,
697+
],
698+
[
699+
'name' => 'data',
700+
'contents' => $data,
701+
],
702+
];
703+
$resp = $this->proxy->put($participantCloudId->getId(), $shareToken, $url, ['multipart' => $params]);
704+
return $this->proxy->getOcsData($resp);
705+
}
706+
$params = [
707+
'boardId' => $localBoard->getExternalId(),
708+
'data' => $data,
709+
'type' => $type,
710+
];
711+
$resp = $this->proxy->put($participantCloudId->getId(), $shareToken, $url, $params);
712+
return $this->proxy->getOcsData($resp);
713+
}
714+
715+
public function deleteAttachmentOnRemote(Board $localBoard, int $cardId, int $attachmentId, string $type = 'file'): array {
716+
$this->configService->ensureFederationEnabled();
717+
$this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false);
718+
$shareToken = $localBoard->getShareToken();
719+
$ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner());
720+
$url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/attachments/' . $attachmentId;
721+
$resp = $this->proxy->delete($ownerCloudId->getId(), $shareToken, $url, [
722+
'boardId' => $localBoard->getExternalId(),
723+
'type' => $type,
724+
]);
725+
return $this->proxy->getOcsData($resp);
726+
}
727+
728+
public function restoreAttachmentOnRemote(Board $localBoard, int $cardId, int $attachmentId, string $type = 'file'): array {
729+
$this->configService->ensureFederationEnabled();
730+
$this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false);
731+
$shareToken = $localBoard->getShareToken();
732+
$participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null);
733+
$ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner());
734+
$url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/attachments/' . $attachmentId . '/restore';
735+
$resp = $this->proxy->put($participantCloudId->getId(), $shareToken, $url, [
736+
'boardId' => $localBoard->getExternalId(),
737+
'type' => $type,
738+
]);
739+
return $this->proxy->getOcsData($resp);
740+
}
589741
}

0 commit comments

Comments
 (0)