Skip to content

Commit c554f12

Browse files
authored
Merge pull request #7330 from nextcloud/feat/federation
Federated board sharing
2 parents a4d1204 + 8ddd4d9 commit c554f12

58 files changed

Lines changed: 2698 additions & 1010 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

appinfo/routes.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,18 @@
133133
['name' => 'board_api#preflighted_cors', 'url' => '/api/v{apiVersion}/{path}','verb' => 'OPTIONS', 'requirements' => ['path' => '.+']],
134134
],
135135
'ocs' => [
136+
['name' => 'board_ocs#index', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'GET'],
137+
['name' => 'board_ocs#read', 'url' => '/api/v{apiVersion}/board/{boardId}', 'verb' => 'GET'],
138+
['name' => 'board_ocs#stacks', 'url' => '/api/v{apiVersion}/stacks/{boardId}', 'verb' => 'GET'],
139+
['name' => 'board_ocs#create', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'POST'],
140+
['name' => 'board_ocs#addAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl', 'verb' => 'POST'],
141+
142+
['name' => 'card_ocs#create', 'url' => '/api/v{apiVersion}/cards', 'verb' => 'POST'],
143+
['name' => 'card_ocs#update', 'url' => '/api/v{apiVersion}/cards/{cardId}', 'verb' => 'PUT'],
144+
145+
['name' => 'stack_ocs#create', 'url' => '/api/v{apiVersion}/stacks', 'verb' => 'POST'],
146+
['name' => 'stack_ocs#delete', 'url' => '/api/v{apiVersion}/stacks/{stackId}/{boardId}', 'verb' => 'DELETE', 'defaults' => ['boardId' => null]],
147+
136148
['name' => 'Config#get', 'url' => '/api/v{apiVersion}/config', 'verb' => 'GET'],
137149
['name' => 'Config#setValue', 'url' => '/api/v{apiVersion}/config/{key}', 'verb' => 'POST'],
138150

cypress/e2e/boardFeatures.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ describe('Board', function() {
2424

2525
cy.intercept({
2626
method: 'POST',
27-
url: '/index.php/apps/deck/boards',
27+
url: '/ocs/v2.php/apps/deck/api/v1.0/boards',
2828
}).as('createBoardRequest')
2929

3030
// Click "Add board"

cypress/e2e/cardFeatures.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('Card', function () {
6666
it('Create card from overview', function () {
6767
cy.visit(`/apps/deck/#/`)
6868
const newCardTitle = 'Test create from overview'
69-
cy.intercept({ method: 'POST', url: '**/apps/deck/cards' }).as('save')
69+
cy.intercept({ method: 'POST', url: '**/ocs/v2.php/apps/deck/api/v1.0/cards' }).as('save')
7070
cy.intercept({ method: 'GET', url: '**/apps/deck/boards/*' }).as('getBoard')
7171

7272
cy.get('.button-vue[aria-label*="Add card"]')
@@ -194,7 +194,7 @@ describe('Card', function () {
194194

195195
it('Shows the modal with the editor', () => {
196196
cy.get('.card:contains("Hello world")').should('be.visible').click()
197-
cy.intercept({ method: 'PUT', url: '**/apps/deck/cards/*' }).as('save')
197+
cy.intercept({ method: 'PUT', url: '**/ocs/v2.php/apps/deck/api/v1.0/cards/*' }).as('save')
198198
cy.get('.modal__card').should('be.visible')
199199
cy.get('.app-sidebar-header__mainname').contains('Hello world')
200200
cy.get('.modal__card .ProseMirror h1').contains('Hello world').should('be.visible')
@@ -213,7 +213,7 @@ describe('Card', function () {
213213

214214
it('Smart picker', () => {
215215
const newCardTitle = 'Test smart picker'
216-
cy.intercept({ method: 'POST', url: '**/apps/deck/cards' }).as('save')
216+
cy.intercept({ method: 'POST', url: '**/ocs/v2.php/apps/deck/api/v1.0/cards' }).as('save')
217217
cy.intercept({ method: 'GET', url: '**/apps/deck/boards/*' }).as('getBoard')
218218
cy.get('.card:contains("Hello world")').should('be.visible').click()
219219
cy.get('.modal__card').should('be.visible')

cypress/e2e/deckDashboard.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('Deck dashboard', function() {
4141
}).then((board) => {
4242
cy.visit(`/apps/deck/#/board/${board.id}`)
4343

44-
cy.intercept({ method: 'PUT', url: '**/apps/deck/cards/**' }).as('updateCard')
44+
cy.intercept({ method: 'PUT', url: '**/ocs/v2.php/apps/deck/api/v1.0/cards/**' }).as('updateCard')
4545

4646
const newCardTitle = 'Hello world'
4747
cy.get(`.card:contains("${newCardTitle}")`).should('be.visible').click()

cypress/support/commands.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Cypress.Commands.add('getNavigationEntry', (boardTitle) => {
9999
})
100100

101101
Cypress.Commands.add('shareBoardWithUi', (query, userId=query) => {
102-
cy.intercept({ method: 'GET', url: `**/ocs/v2.php/apps/files_sharing/api/v1/sharees?search=${query}*` }).as('fetchRecipients')
102+
cy.intercept({ method: 'GET', url: `**/ocs/v2.php/core/autocomplete/get?search=${query}*` }).as('fetchRecipients')
103103
cy.get('[aria-label="Open details"]').click()
104104
cy.get('.app-sidebar').should('be.visible')
105105

lib/Activity/ActivityManager.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ public function triggerUpdateEvents($objectType, ChangeSet $changeSet, $subject)
270270
];
271271
if ($changes['before'] !== $changes['after']) {
272272
try {
273-
$event = $this->createEvent($objectType, $entity, $subjectComplete, $changes);
273+
$event = $this->createEvent($objectType, $entity, $subjectComplete, $changes, $this->userId);
274274
if ($event !== null) {
275275
$events[] = $event;
276276
}
@@ -300,6 +300,11 @@ public function triggerUpdateEvents($objectType, ChangeSet $changeSet, $subject)
300300
* @throws \Exception
301301
*/
302302
private function createEvent($objectType, $entity, $subject, $additionalParams = [], $author = null) {
303+
// @TODO implement actual activities for federated users
304+
// this case only happens for federated activities if the author is not provided
305+
if ($author === null && $this->userId === null) {
306+
return;
307+
}
303308
try {
304309
$object = $this->findObjectForEntity($objectType, $entity);
305310
} catch (DoesNotExistException $e) {

lib/AppInfo/Application.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use OCA\Deck\Event\CardUpdatedEvent;
2828
use OCA\Deck\Event\SessionClosedEvent;
2929
use OCA\Deck\Event\SessionCreatedEvent;
30+
use OCA\Deck\Federation\DeckFederationProvider;
3031
use OCA\Deck\Listeners\AclCreatedRemovedListener;
3132
use OCA\Deck\Listeners\BeforeTemplateRenderedListener;
3233
use OCA\Deck\Listeners\CommentEventListener;
@@ -35,8 +36,10 @@
3536
use OCA\Deck\Listeners\ParticipantCleanupListener;
3637
use OCA\Deck\Listeners\ResourceAdditionalScriptsListener;
3738
use OCA\Deck\Listeners\ResourceListener;
39+
use OCA\Deck\Listeners\ResourceTypeRegisterListener;
3840
use OCA\Deck\Middleware\DefaultBoardMiddleware;
3941
use OCA\Deck\Middleware\ExceptionMiddleware;
42+
use OCA\Deck\Middleware\FederationMiddleware;
4043
use OCA\Deck\Notification\Notifier;
4144
use OCA\Deck\Reference\BoardReferenceProvider;
4245
use OCA\Deck\Reference\CardReferenceProvider;
@@ -60,9 +63,13 @@
6063
use OCP\Comments\CommentsEntityEvent;
6164
use OCP\Comments\CommentsEvent;
6265
use OCP\EventDispatcher\IEventDispatcher;
66+
use OCP\Federation\ICloudFederationProvider;
67+
use OCP\Federation\ICloudFederationProviderManager;
6368
use OCP\Group\Events\GroupDeletedEvent;
6469
use OCP\IConfig;
6570
use OCP\IDBConnection;
71+
use OCP\OCM\Events\ResourceTypeRegisterEvent;
72+
use OCP\Server;
6673
use OCP\Share\IManager;
6774
use OCP\User\Events\UserDeletedEvent;
6875
use OCP\Util;
@@ -102,6 +109,7 @@ public function boot(IBootContext $context): void {
102109
$context->injectFn(function (Listener $listener, IEventDispatcher $eventDispatcher) {
103110
$listener->register($eventDispatcher);
104111
});
112+
$context->injectFn([$this, 'registerCloudFederationProviderManager']);
105113
}
106114

107115
public function register(IRegistrationContext $context): void {
@@ -110,6 +118,7 @@ public function register(IRegistrationContext $context): void {
110118
}
111119

112120
$context->registerCapability(Capabilities::class);
121+
$context->registerMiddleWare(FederationMiddleware::class);
113122
$context->registerMiddleWare(ExceptionMiddleware::class);
114123
$context->registerMiddleWare(DefaultBoardMiddleware::class);
115124

@@ -134,6 +143,7 @@ public function register(IRegistrationContext $context): void {
134143
$context->registerReferenceProvider(CommentReferenceProvider::class);
135144

136145
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
146+
$context->registerEventListener(ResourceTypeRegisterEvent::class, ResourceTypeRegisterListener::class);
137147

138148
// Event listening to emit UserShareAccessUpdatedEvent for files_sharing
139149
$context->registerEventListener(AclCreatedEvent::class, AclCreatedRemovedListener::class);
@@ -194,4 +204,15 @@ protected function registerCollaborationResources(IProviderManager $resourceMana
194204
$resourceManager->registerResourceProvider(ResourceProvider::class);
195205
$resourceManager->registerResourceProvider(ResourceProviderCard::class);
196206
}
207+
208+
public function registerCloudFederationProviderManager(
209+
IConfig $config,
210+
ICloudFederationProviderManager $manager,
211+
): void {
212+
$manager->addCloudFederationProvider(
213+
DeckFederationProvider::PROVIDER_ID,
214+
'Deck Federation',
215+
static fn (): ICloudFederationProvider => Server::get(DeckFederationProvider::class),
216+
);
217+
}
197218
}

lib/Controller/BoardController.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use OCA\Deck\Db\Board;
1212
use OCA\Deck\NoPermissionException;
1313
use OCA\Deck\Service\BoardService;
14+
use OCA\Deck\Service\ExternalBoardService;
1415
use OCA\Deck\Service\Importer\BoardImportService;
1516
use OCA\Deck\Service\PermissionService;
1617
use OCP\AppFramework\ApiController;
@@ -25,6 +26,7 @@ public function __construct(
2526
$appName,
2627
IRequest $request,
2728
private BoardService $boardService,
29+
private ExternalBoardService $externalBoardService,
2830
private PermissionService $permissionService,
2931
private BoardImportService $boardImportService,
3032
private IL10N $l10n,
@@ -83,7 +85,7 @@ public function getUserPermissions(int $boardId): array {
8385
* @param $participant
8486
*/
8587
#[NoAdminRequired]
86-
public function addAcl(int $boardId, int $type, $participant, bool $permissionEdit, bool $permissionShare, bool $permissionManage): Acl {
88+
public function addAcl(int $boardId, int $type, $participant, bool $permissionEdit, bool $permissionShare, bool $permissionManage, ?string $remote = null): Acl {
8789
return $this->boardService->addAcl($boardId, $type, $participant, $permissionEdit, $permissionShare, $permissionManage);
8890
}
8991

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Deck\Controller;
9+
10+
use OCA\Deck\Service\BoardService;
11+
use OCA\Deck\Service\ExternalBoardService;
12+
use OCA\Deck\Service\StackService;
13+
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
14+
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
15+
use OCP\AppFramework\Http\Attribute\PublicPage;
16+
use OCP\AppFramework\Http\Attribute\RequestHeader;
17+
use OCP\AppFramework\Http\DataResponse;
18+
use OCP\AppFramework\OCSController;
19+
use OCP\IRequest;
20+
use Psr\Log\LoggerInterface;
21+
22+
class BoardOcsController extends OCSController {
23+
public function __construct(
24+
string $appName,
25+
IRequest $request,
26+
private BoardService $boardService,
27+
private ExternalBoardService $externalBoardService,
28+
private LoggerInterface $logger,
29+
private StackService $stackService,
30+
private $userId,
31+
) {
32+
parent::__construct($appName, $request);
33+
}
34+
35+
#[NoAdminRequired]
36+
public function index(): DataResponse {
37+
$internalBoards = $this->boardService->findAll();
38+
return new DataResponse($internalBoards);
39+
}
40+
41+
#[NoAdminRequired]
42+
#[PublicPage]
43+
#[NoCSRFRequired]
44+
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
45+
public function read(int $boardId): DataResponse {
46+
// Board on this instance -> get it from database
47+
$localBoard = $this->boardService->find($boardId, true, true);
48+
if ($localBoard->getExternalId() !== null) {
49+
return $this->externalBoardService->getExternalBoardFromRemote($localBoard);
50+
}
51+
// Board on other instance -> get it from other instance
52+
return new DataResponse($localBoard);
53+
}
54+
55+
#[NoAdminRequired]
56+
#[NoCSRFRequired]
57+
public function create(string $title, string $color): DataResponse {
58+
return new DataResponse($this->boardService->create($title, $this->userId, $color));
59+
}
60+
61+
#[NoAdminRequired]
62+
#[PublicPage]
63+
#[NoCSRFRequired]
64+
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
65+
public function stacks(int $boardId): DataResponse {
66+
$localBoard = $this->boardService->find($boardId, true, true);
67+
// Board on other instance -> get it from other instance
68+
if ($localBoard->getExternalId() !== null) {
69+
return $this->externalBoardService->getExternalStacksFromRemote($localBoard);
70+
} else {
71+
return new DataResponse($this->stackService->findAll($boardId));
72+
}
73+
}
74+
75+
#[NoAdminRequired]
76+
#[NoCSRFRequired]
77+
public function addAcl(int $boardId, int $type, string $participant, bool $permissionEdit, bool $permissionShare, bool $permissionManage, ?string $remote = null): DataResponse {
78+
return new DataResponse($this->boardService->addAcl($boardId, $type, $participant, $permissionEdit, $permissionShare, $permissionManage));
79+
}
80+
81+
#[NoAdminRequired]
82+
#[NoCSRFRequired]
83+
public function updateAcl(int $id, bool $permissionEdit, bool $permissionShare, bool $permissionManage): DataResponse {
84+
return new DataResponse($this->boardService->updateAcl($id, $permissionEdit, $permissionShare, $permissionManage));
85+
}
86+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Deck\Controller;
9+
10+
use OCA\Deck\Model\OptionalNullableValue;
11+
use OCA\Deck\Service\BoardService;
12+
use OCA\Deck\Service\CardService;
13+
use OCA\Deck\Service\ExternalBoardService;
14+
use OCA\Deck\Service\StackService;
15+
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
16+
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
17+
use OCP\AppFramework\Http\Attribute\PublicPage;
18+
use OCP\AppFramework\Http\Attribute\RequestHeader;
19+
use OCP\AppFramework\Http\DataResponse;
20+
use OCP\AppFramework\OCSController;
21+
use OCP\IRequest;
22+
23+
class CardOcsController extends OCSController {
24+
public function __construct(
25+
string $appName,
26+
IRequest $request,
27+
private CardService $cardService,
28+
private StackService $stackService,
29+
private BoardService $boardService,
30+
private ExternalBoardService $externalBoardService,
31+
private ?string $userId,
32+
) {
33+
parent::__construct($appName, $request);
34+
}
35+
36+
#[NoAdminRequired]
37+
#[PublicPage]
38+
#[NoCSRFRequired]
39+
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
40+
public function create(string $title, int $stackId, ?int $boardId = null, ?string $type = 'plain', ?string $owner = null, ?int $order = 999, ?string $description = '', $duedate = null, ?array $labels = [], ?array $users = []) {
41+
if ($boardId) {
42+
$board = $this->boardService->find($boardId, false);
43+
if ($board->getExternalId()) {
44+
$card = $this->externalBoardService->createCardOnRemote($board, $title, $stackId, $type, $order, $description, $duedate, $users);
45+
return new DataResponse($card);
46+
}
47+
}
48+
49+
if (!$owner) {
50+
$owner = $this->userId;
51+
}
52+
$card = $this->cardService->create($title, $stackId, $type, $order, $owner, $description, $duedate);
53+
54+
// foreach ($labels as $label) {
55+
// $this->assignLabel($card->getId(), $label);
56+
// }
57+
58+
// foreach ($users as $user) {
59+
// $this->assignmentService->assignUser($card->getId(), $user['id'], $user['type']);
60+
// }
61+
62+
return new DataResponse($card);
63+
}
64+
65+
#[NoAdminRequired]
66+
#[PublicPage]
67+
#[NoCSRFRequired]
68+
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
69+
public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, int $boardId, array|string|null $owner = null, $archived = null): DataResponse {
70+
$done = array_key_exists('done', $this->request->getParams())
71+
? new OptionalNullableValue($this->request->getParam('done', null))
72+
: null;
73+
if (!$owner) {
74+
$owner = $this->userId;
75+
} else {
76+
if (!is_string($owner)) {
77+
$owner = $owner['uid'];
78+
}
79+
}
80+
81+
$localBoard = $this->boardService->find($boardId, false);
82+
if ($localBoard->getExternalId()) {
83+
return new DataResponse($this->externalBoardService->updateCardOnRemote(
84+
$localBoard,
85+
$id,
86+
$title,
87+
$stackId,
88+
$type,
89+
$owner,
90+
$description,
91+
$order,
92+
$duedate,
93+
$deletedAt,
94+
$archived,
95+
$done
96+
));
97+
}
98+
99+
return new DataResponse($this->cardService->update($id,
100+
$title,
101+
$stackId,
102+
$type,
103+
$owner,
104+
$description,
105+
$order,
106+
$duedate,
107+
$deletedAt,
108+
$archived,
109+
$done
110+
));
111+
}
112+
}

0 commit comments

Comments
 (0)