Skip to content

Commit de1dd8d

Browse files
committed
ex app harp
Signed-off-by: Hoang Pham <hoangmaths96@gmail.com>
1 parent f58b706 commit de1dd8d

File tree

17 files changed

+3504
-6
lines changed

17 files changed

+3504
-6
lines changed

REUSE.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ SPDX-PackageSupplier = "Nextcloud <info@nextcloud.com>"
66
SPDX-PackageDownloadLocation = "https://github.com/nextcloud/whiteboard"
77

88
[[annotations]]
9-
path = [".gitattributes", ".editorconfig", "babel.config.js", ".php-cs-fixer.dist.php", "package-lock.json", "package.json", "composer.json", "composer.lock", "webpack.js", "stylelint.config.js", ".eslintrc.js", "cypress/.eslintrc.json", ".gitignore", ".jshintrc", ".l10nignore", "action/.gitignore", "action/package.json", "action/package-lock.json", "action/dist/index.js", "tests/**", "psalm.xml", "vendor-bin/**/composer.json", "vendor-bin/**/composer.lock", ".tx/config", "webpack.config.js", "js/vendor.LICENSE.txt", ".github/CODEOWNERS", "vite.config.js", "stylelint.config.cjs", "composer/**.php", "composer/composer.**", "tsconfig.json", "jsconfig.json", "krankerl.toml", "renovate.json", ".github/ISSUE_TEMPLATE/**", ".nextcloudignore", "CHANGELOG.md", ".tsconfig.json"]
9+
path = [".gitattributes", ".editorconfig", "babel.config.js", ".php-cs-fixer.dist.php", "package-lock.json", "package.json", "composer.json", "composer.lock", "webpack.js", "stylelint.config.js", ".eslintrc.js", "cypress/.eslintrc.json", ".gitignore", ".jshintrc", ".l10nignore", "action/.gitignore", "action/package.json", "action/package-lock.json", "action/dist/index.js", "tests/**", "psalm.xml", "vendor-bin/**/composer.json", "vendor-bin/**/composer.lock", ".tx/config", "webpack.config.js", "js/vendor.LICENSE.txt", ".github/CODEOWNERS", "vite.config.js", "stylelint.config.cjs", "composer/**.php", "composer/composer.**", "tsconfig.json", "jsconfig.json", "krankerl.toml", "renovate.json", ".github/ISSUE_TEMPLATE/**", ".nextcloudignore", "CHANGELOG.md", ".tsconfig.json", "websocket_server/package.json", "websocket_server/package-lock.json"]
1010
precedence = "aggregate"
1111
SPDX-FileCopyrightText = "none"
1212
SPDX-License-Identifier = "CC0-1.0"

lib/AppInfo/Application.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010

1111
namespace OCA\Whiteboard\AppInfo;
1212

13+
use OCA\AppAPI\Middleware\AppAPIAuthMiddleware;
1314
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
1415
use OCA\Viewer\Event\LoadViewer;
1516
use OCA\Whiteboard\Listener\AddContentSecurityPolicyListener;
1617
use OCA\Whiteboard\Listener\BeforeTemplateRenderedListener;
1718
use OCA\Whiteboard\Listener\LoadViewerListener;
1819
use OCA\Whiteboard\Listener\RegisterTemplateCreatorListener;
20+
use OCA\Whiteboard\Service\ConfigService;
21+
use OCA\Whiteboard\Service\ExAppService;
1922
use OCA\Whiteboard\Settings\SetupCheck;
2023
use OCP\AppFramework\App;
2124
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -24,8 +27,12 @@
2427
use OCP\Files\Template\ITemplateManager;
2528
use OCP\Files\Template\RegisterTemplateCreatorEvent;
2629
use OCP\IL10N;
30+
use OCP\IURLGenerator;
2731
use OCP\Security\CSP\AddContentSecurityPolicyEvent;
2832
use OCP\Util;
33+
use Psr\Container\ContainerExceptionInterface;
34+
use Psr\Container\NotFoundExceptionInterface;
35+
use Psr\Log\LoggerInterface;
2936

3037
/**
3138
* @psalm-suppress UndefinedClass
@@ -47,6 +54,13 @@ public function register(IRegistrationContext $context): void {
4754
$context->registerEventListener(RegisterTemplateCreatorEvent::class, RegisterTemplateCreatorListener::class);
4855
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
4956
$context->registerSetupCheck(SetupCheck::class);
57+
58+
if (class_exists(AppAPIAuthMiddleware::class) && $this->getExAppService()->isWhiteboardWebsocketEnabled()) {
59+
$context->registerMiddleware(AppAPIAuthMiddleware::class);
60+
}
61+
62+
// Auto-configure collaboration URL and JWT secret if ExApp is detected
63+
$this->configureExAppCollaboration();
5064
}
5165

5266
#[\Override]
@@ -60,4 +74,81 @@ public function boot(IBootContext $context): void {
6074
});
6175
}
6276
}
77+
78+
/**
79+
* Automatically configure collaboration URL and JWT secret when ExApp is detected
80+
*/
81+
private function configureExAppCollaboration(): void {
82+
try {
83+
$container = $this->getContainer();
84+
$exAppService = $container->get(ExAppService::class);
85+
$configService = $container->get(ConfigService::class);
86+
$urlGenerator = $container->get(IURLGenerator::class);
87+
88+
if ($exAppService->isWhiteboardWebsocketEnabled()) {
89+
// Generate the ExApp collaboration URL
90+
$baseUrl = $urlGenerator->getAbsoluteURL('');
91+
$exAppUrl = rtrim($baseUrl, '/') . '/exapps/nextcloud_whiteboard';
92+
93+
// Check current URL configuration
94+
$currentUrl = $configService->getCollabBackendUrl();
95+
96+
// Force update to ExApp URL when ExApp is detected (for dynamic configuration)
97+
if ($currentUrl !== $exAppUrl) {
98+
$configService->setCollabBackendUrl($exAppUrl);
99+
}
100+
101+
// Configure JWT secret synchronization with ExApp
102+
$this->configureExAppJwtSecret($exAppService, $configService);
103+
}
104+
} catch (\Exception) {
105+
// Silently fail - this is auto-configuration, shouldn't break app registration
106+
}
107+
}
108+
109+
/**
110+
* Configure JWT secret synchronization between Nextcloud and ExApp
111+
*/
112+
private function configureExAppJwtSecret(ExAppService $exAppService, ConfigService $configService): void {
113+
try {
114+
$logger = $this->getContainer()->get(LoggerInterface::class);
115+
$logger->debug('Starting JWT secret synchronization for ExApp');
116+
117+
// Get the ExApp secret from app_api
118+
$exAppSecret = $exAppService->getWhiteboardExAppSecret();
119+
120+
if ($exAppSecret !== null && $exAppSecret !== '') {
121+
$logger->debug('ExApp secret retrieved successfully');
122+
123+
// Get current JWT secret from whiteboard config
124+
$currentJwtSecret = $configService->getJwtSecretKey();
125+
126+
// Update JWT secret if it's different from ExApp secret
127+
if ($currentJwtSecret !== $exAppSecret) {
128+
$logger->info('Updating whiteboard JWT secret to match ExApp secret');
129+
$configService->setWhiteboardSharedSecret($exAppSecret);
130+
} else {
131+
$logger->debug('JWT secret already matches ExApp secret, no update needed');
132+
}
133+
} else {
134+
$logger->warning('ExApp secret is null or empty, cannot synchronize JWT secret');
135+
}
136+
} catch (\Exception $e) {
137+
// Log the error but don't break app registration
138+
try {
139+
$logger = $this->getContainer()->get(LoggerInterface::class);
140+
$logger->error('Failed to configure ExApp JWT secret', ['error' => $e->getMessage()]);
141+
} catch (\Exception) {
142+
// Silently fail if we can't even get the logger
143+
}
144+
}
145+
}
146+
147+
/**
148+
* @throws ContainerExceptionInterface
149+
* @throws NotFoundExceptionInterface
150+
*/
151+
private function getExAppService(): ExAppService {
152+
return $this->getContainer()->get(ExAppService::class);
153+
}
63154
}

lib/Consts/ExAppConsts.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OCA\Whiteboard\Consts;
11+
12+
final class ExAppConsts {
13+
public const APP_API_ID = 'app_api';
14+
public const WHITEBOARD_EX_APP_ID = 'nextcloud_whiteboard';
15+
public const WHITEBOARD_EX_APP_ENABLED_KEY = 'isWhiteboardExAppEnabled';
16+
}

lib/Listener/LoadViewerListener.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
use OCA\Viewer\Event\LoadViewer;
1414
use OCA\Whiteboard\Service\ConfigService;
15+
use OCA\Whiteboard\Service\ExAppService;
1516
use OCP\AppFramework\Services\IInitialState;
1617
use OCP\EventDispatcher\Event;
1718
use OCP\EventDispatcher\IEventListener;
@@ -22,6 +23,7 @@ class LoadViewerListener implements IEventListener {
2223
public function __construct(
2324
private IInitialState $initialState,
2425
private ConfigService $configService,
26+
private ExAppService $exAppService,
2527
) {
2628
}
2729

@@ -42,5 +44,8 @@ public function handle(Event $event): void {
4244
'maxFileSize',
4345
$this->configService->getMaxFileSize()
4446
);
47+
48+
// Initialize ExApp frontend state
49+
$this->exAppService->initFrontendState();
4550
}
4651
}

lib/Service/ExAppService.php

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Whiteboard\Service;
11+
12+
use OCA\AppAPI\Service\ExAppService as AppAPIService;
13+
use OCA\Whiteboard\Consts\ExAppConsts;
14+
use OCP\App\IAppManager;
15+
use OCP\AppFramework\Services\IInitialState;
16+
use OCP\IDBConnection;
17+
use Psr\Container\ContainerInterface;
18+
use Psr\Log\LoggerInterface;
19+
use Throwable;
20+
21+
/**
22+
* @psalm-suppress UndefinedClass
23+
* @psalm-suppress MissingDependency
24+
*/
25+
final class ExAppService {
26+
private ?AppAPIService $appAPIService = null;
27+
28+
public function __construct(
29+
private IAppManager $appManager,
30+
private ContainerInterface $container,
31+
private IInitialState $initialState,
32+
private LoggerInterface $logger,
33+
private IDBConnection $dbConnection,
34+
) {
35+
$this->initAppAPIService();
36+
}
37+
38+
private function initAppAPIService(): void {
39+
$isAppAPIEnabled = $this->isAppAPIEnabled();
40+
41+
if (class_exists(AppAPIService::class) && $isAppAPIEnabled) {
42+
try {
43+
$this->appAPIService = $this->container->get(AppAPIService::class);
44+
} catch (Throwable $e) {
45+
$this->logger->error('exApp', [$e->getMessage()]);
46+
}
47+
}
48+
}
49+
50+
private function isAppAPIEnabled(): bool {
51+
return $this->appManager->isEnabledForUser(ExAppConsts::APP_API_ID);
52+
}
53+
54+
public function isExAppEnabled(string $appId): bool {
55+
if ($this->appAPIService === null) {
56+
return false;
57+
}
58+
59+
return $this->appAPIService->getExApp($appId)?->getEnabled() === 1;
60+
}
61+
62+
public function isWhiteboardWebsocketEnabled(): bool {
63+
return $this->isExAppEnabled(ExAppConsts::WHITEBOARD_EX_APP_ID);
64+
}
65+
66+
public function initFrontendState(): void {
67+
$this->initialState->provideInitialState(
68+
ExAppConsts::WHITEBOARD_EX_APP_ENABLED_KEY,
69+
$this->isWhiteboardWebsocketEnabled()
70+
);
71+
}
72+
73+
/**
74+
* Get the ExApp secret for the whiteboard ExApp
75+
* This secret is used for JWT authentication between Nextcloud and the ExApp
76+
*/
77+
public function getWhiteboardExAppSecret(): ?string {
78+
if ($this->appAPIService === null) {
79+
return null;
80+
}
81+
82+
try {
83+
$exApp = $this->appAPIService->getExApp(ExAppConsts::WHITEBOARD_EX_APP_ID);
84+
if ($exApp === null) {
85+
$this->logger->debug('ExApp not found', ['appId' => ExAppConsts::WHITEBOARD_EX_APP_ID]);
86+
return null;
87+
}
88+
89+
// Try to get the secret using the getSecret method
90+
try {
91+
if (method_exists($exApp, 'getSecret')) {
92+
$secret = $exApp->getSecret();
93+
if ($secret !== null && $secret !== '') {
94+
$this->logger->debug('ExApp secret retrieved successfully via getSecret method');
95+
return $secret;
96+
}
97+
}
98+
} catch (Throwable $e) {
99+
$this->logger->warning('Failed to get secret via getSecret method', [
100+
'error' => $e->getMessage()
101+
]);
102+
}
103+
104+
$this->logger->info('ExApp secret not accessible via getSecret method, trying database fallback', [
105+
'appId' => ExAppConsts::WHITEBOARD_EX_APP_ID
106+
]);
107+
108+
// Fallback: try to get secret directly from database
109+
return $this->getExAppSecretFromDatabase(ExAppConsts::WHITEBOARD_EX_APP_ID);
110+
} catch (Throwable $e) {
111+
$this->logger->error('Failed to retrieve ExApp secret', [
112+
'appId' => ExAppConsts::WHITEBOARD_EX_APP_ID,
113+
'error' => $e->getMessage()
114+
]);
115+
116+
// Final fallback: try database access
117+
return $this->getExAppSecretFromDatabase(ExAppConsts::WHITEBOARD_EX_APP_ID);
118+
}
119+
}
120+
121+
/**
122+
* Fallback method to get ExApp secret directly from database
123+
*/
124+
private function getExAppSecretFromDatabase(string $appId): ?string {
125+
try {
126+
$this->logger->debug('Attempting to retrieve ExApp secret from database', ['appId' => $appId]);
127+
128+
$qb = $this->dbConnection->getQueryBuilder();
129+
$qb->select('secret')
130+
->from('ex_apps')
131+
->where($qb->expr()->eq('appid', $qb->createNamedParameter($appId)));
132+
133+
$result = $qb->executeQuery();
134+
$row = $result->fetch();
135+
$result->closeCursor();
136+
137+
if ($row && isset($row['secret']) && $row['secret'] !== '') {
138+
$this->logger->debug('ExApp secret retrieved successfully from database');
139+
return $row['secret'];
140+
}
141+
142+
$this->logger->warning('ExApp secret not found in database or is empty', ['appId' => $appId]);
143+
return null;
144+
} catch (Throwable $e) {
145+
$this->logger->error('Failed to retrieve ExApp secret from database', [
146+
'appId' => $appId,
147+
'error' => $e->getMessage()
148+
]);
149+
return null;
150+
}
151+
}
152+
}

lib/Settings/Admin.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace OCA\Whiteboard\Settings;
99

1010
use OCA\Whiteboard\Service\ConfigService;
11+
use OCA\Whiteboard\Service\ExAppService;
1112
use OCA\Whiteboard\Service\JWTService;
1213
use OCP\AppFramework\Http\ContentSecurityPolicy;
1314
use OCP\AppFramework\Http\TemplateResponse;
@@ -19,6 +20,7 @@ public function __construct(
1920
private IInitialState $initialState,
2021
private ConfigService $configService,
2122
private JWTService $jwtService,
23+
private ExAppService $exAppService,
2224
) {
2325
}
2426

@@ -44,6 +46,10 @@ public function getForm(): TemplateResponse {
4446

4547
#[\Override]
4648
public function getSection() {
49+
if ($this->exAppService->isWhiteboardWebsocketEnabled()) {
50+
return null;
51+
}
52+
4753
return 'whiteboard';
4854
}
4955

src/hooks/useCollaboration.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ export function useCollaboration() {
4848
})),
4949
)
5050

51-
const { fileId } = useWhiteboardConfigStore(
51+
const { fileId, collabBackendUrl } = useWhiteboardConfigStore(
5252
useShallow(state => ({
5353
fileId: state.fileId,
54+
collabBackendUrl: state.collabBackendUrl,
5455
})),
5556
)
5657

@@ -507,7 +508,6 @@ export function useCollaboration() {
507508
try {
508509
setStatus('connecting')
509510
// Get collaboration backend URL from the WhiteboardConfigStore
510-
const collabBackendUrl = useWhiteboardConfigStore.getState().collabBackendUrl
511511
if (!collabBackendUrl) throw new Error('Collaboration backend URL missing.')
512512

513513
const token = await getJWT()
@@ -567,7 +567,7 @@ export function useCollaboration() {
567567
}
568568
}, [
569569
getJWT, setStatus, setSocket, setupSocketEventHandlers,
570-
fileId,
570+
fileId, collabBackendUrl,
571571
])
572572

573573
useEffect(() => {

tests/psalm-baseline.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
<code><![CDATA[__construct]]></code>
5959
</PossiblyUnusedMethod>
6060
</file>
61+
6162
<file src="lib/Service/ExceptionService.php">
6263
<ArgumentTypeCoercion>
6364
<code><![CDATA[$statusCode]]></code>

0 commit comments

Comments
 (0)