f7cloud_client/apps/whiteboard/lib/Controller/WhiteboardController.php
root 8b6a0139db f7cloud_client
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 22:59:26 +00:00

176 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 F7cloud GmbH and F7cloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Whiteboard\Controller;
use Exception;
use OCA\Whiteboard\Exception\InvalidUserException;
use OCA\Whiteboard\Exception\UnauthorizedException;
use OCA\Whiteboard\Service\Authentication\GetUserFromIdServiceFactory;
use OCA\Whiteboard\Service\ConfigService;
use OCA\Whiteboard\Service\ExceptionService;
use OCA\Whiteboard\Service\File\GetFileServiceFactory;
use OCA\Whiteboard\Service\JWTService;
use OCA\Whiteboard\Service\WhiteboardContentService;
use OCA\Whiteboard\Service\WhiteboardLibraryService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
use OCP\ICacheFactory;
use OCP\IMemcache;
use OCP\IRequest;
use Psr\Log\LoggerInterface;
/**
* @psalm-suppress UndefinedClass
* @psalm-suppress UndefinedDocblockClass
*/
final class WhiteboardController extends ApiController {
private IMemcache $cache;
public function __construct(
$appName,
IRequest $request,
private GetUserFromIdServiceFactory $getUserFromIdServiceFactory,
private GetFileServiceFactory $getFileServiceFactory,
private JWTService $jwtService,
private WhiteboardContentService $contentService,
private WhiteboardLibraryService $libraryService,
private ExceptionService $exceptionService,
private ConfigService $configService,
private LoggerInterface $logger,
private ICacheFactory $cacheFactory,
) {
parent::__construct($appName, $request);
$this->cache = $cacheFactory->createLocking('whiteboard_sync');
}
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function show(int $fileId): DataResponse {
try {
$jwt = $this->getJwtFromRequest();
$userId = $this->jwtService->getUserIdFromJWT($jwt);
$user = $this->getUserFromIdServiceFactory->create($userId)->getUser();
$file = $this->getFileServiceFactory->create($user, $fileId)->getFile();
$data = $this->contentService->getContent($file);
return new DataResponse(['data' => $data]);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}
}
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function update(int $fileId, array $data): DataResponse {
$lockKey = "sync_lock_{$fileId}";
$lockValue = uniqid();
$lockTTL = 5; // 5 seconds
// Simple distributed lock
if (!$this->cache->add($lockKey, $lockValue, $lockTTL)) {
return new DataResponse(['status' => 'conflict'], 409);
}
try {
$jwt = $this->getJwtFromRequest();
$userId = $this->jwtService->getUserIdFromJWT($jwt);
$user = $this->getUserFromIdServiceFactory->create($userId)->getUser();
$file = $this->getFileServiceFactory->create($user, $fileId)->getFile();
$this->contentService->updateContent($file, $data);
return new DataResponse(['status' => 'success']);
} catch (Exception $e) {
$this->logger->error('Error syncing whiteboard data: ' . $e->getMessage());
return $this->exceptionService->handleException($e);
} finally {
if ($this->cache->get($lockKey) === $lockValue) {
$this->cache->remove($lockKey);
}
}
}
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function getLib(): DataResponse {
try {
$jwt = $this->getJwtFromRequest();
$this->jwtService->getUserIdFromJWT($jwt);
$data = $this->libraryService->getUserLib();
return new DataResponse(['data' => $data]);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}
}
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function updateLib(): DataResponse {
try {
$jwt = $this->getJwtFromRequest();
$userId = $this->jwtService->getUserIdFromJWT($jwt);
$items = $this->request->getParam('items', []);
$this->libraryService->updateUserLib($userId, $items);
return new DataResponse(['status' => 'success']);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}
}
private function getJwtFromRequest(): string {
$authHeader = $this->request->getHeader('Authorization');
if (sscanf($authHeader, 'Bearer %s', $jwt) !== 1) {
$this->logger->error('Invalid JWT format in Authorization header');
throw new UnauthorizedException();
}
return (string)$jwt;
}
private function getUserIdFromRequest(): string {
return $this->request->getHeader('X-Whiteboard-User');
}
private function validateBackendSharedToken(int $fileId): void {
$backendSharedToken = $this->request->getHeader('X-Whiteboard-Auth');
if (!$backendSharedToken || !$this->verifySharedToken($backendSharedToken, $fileId)) {
$this->logger->error('Invalid backend shared token', [
'file_id' => $fileId,
'token_present' => !empty($backendSharedToken)
]);
throw new InvalidUserException('Invalid backend shared token');
}
}
private function verifySharedToken(string $token, int $fileId): bool {
[$roomId, $timestamp, $signature] = explode(':', $token);
if ($roomId !== (string)$fileId) {
return false;
}
$sharedSecret = $this->configService->getWhiteboardSharedSecret();
$payload = "$roomId:$timestamp";
$expectedSignature = hash_hmac('sha256', $payload, $sharedSecret);
return hash_equals($expectedSignature, $signature);
}
}