diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index ebc939e..74f6b1f 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -20,6 +20,9 @@ use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\Files\Template\RegisterTemplateCreatorEvent; use OCP\Security\CSP\AddContentSecurityPolicyEvent; +use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent; +use OCA\Whiteboard\Listener\BeforeTemplateRenderedListener; + class Application extends App implements IBootstrap { public const APP_ID = 'whiteboard'; @@ -34,6 +37,7 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(AddContentSecurityPolicyEvent::class, AddContentSecurityPolicyListener::class); $context->registerEventListener(LoadViewer::class, LoadViewerListener::class); $context->registerEventListener(RegisterTemplateCreatorEvent::class, RegisterTemplateCreatorListener::class); + $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); } public function boot(IBootContext $context): void { diff --git a/lib/Controller/JWTController.php b/lib/Controller/JWTController.php index 7435ada..5c65803 100644 --- a/lib/Controller/JWTController.php +++ b/lib/Controller/JWTController.php @@ -35,12 +35,14 @@ public function __construct( /** * @NoCSRFRequired * @NoAdminRequired + * @PublicPage */ public function getJWT(int $fileId): DataResponse { try { - $user = $this->authService->getAuthenticatedUser(); - $file = $this->fileService->getUserFileById($user->getUID(), $fileId); - $jwt = $this->jwtService->generateJWT($user, $file, $fileId); + $publicSharingToken = $this->request->getParam('publicSharingToken'); + $user = $this->authService->getAuthenticatedUser($publicSharingToken); + $file = $this->fileService->getFile($user, $fileId); + $jwt = $this->jwtService->generateJWT($user, $file); return new DataResponse(['token' => $jwt]); } catch (\Exception $e) { return $this->exceptionService->handleException($e); diff --git a/lib/Controller/WhiteboardController.php b/lib/Controller/WhiteboardController.php index 99b6568..f9f4a89 100644 --- a/lib/Controller/WhiteboardController.php +++ b/lib/Controller/WhiteboardController.php @@ -13,6 +13,7 @@ use OCA\Whiteboard\Service\AuthenticationService; use OCA\Whiteboard\Service\ExceptionService; use OCA\Whiteboard\Service\FileService; +use OCA\Whiteboard\Service\JWTService; use OCA\Whiteboard\Service\WhiteboardContentService; use OCP\AppFramework\ApiController; use OCP\AppFramework\Http\Attribute\NoAdminRequired; @@ -20,15 +21,19 @@ use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\IRequest; +use RuntimeException; +use OCP\AppFramework\Http; /** * @psalm-suppress UndefinedClass * @psalm-suppress UndefinedDocblockClass */ -final class WhiteboardController extends ApiController { +final class WhiteboardController extends ApiController +{ public function __construct( $appName, IRequest $request, + private JWTService $jwtService, private AuthenticationService $authService, private FileService $fileService, private WhiteboardContentService $contentService, @@ -40,10 +45,16 @@ public function __construct( #[NoAdminRequired] #[NoCSRFRequired] #[PublicPage] - public function show(int $fileId): DataResponse { + public function show(int $fileId): DataResponse + { try { - $userId = $this->authService->authenticateJWT($this->request); - $file = $this->fileService->getUserFileById($userId, $fileId); + $authHeader = $this->request->getHeader('Authorization'); + if (sscanf($authHeader, 'Bearer %s', $jwt) !== 1) { + throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED); + } + $userId = $this->jwtService->getUserIdFromJWT($jwt); + $user = $this->authService->getWhiteboardUser($userId); + $file = $this->fileService->getFile($user, $fileId); $data = $this->contentService->getContent($file); return new DataResponse(['data' => $data]); } catch (Exception $e) { @@ -54,11 +65,14 @@ public function show(int $fileId): DataResponse { #[NoAdminRequired] #[NoCSRFRequired] #[PublicPage] - public function update(int $fileId, array $data): DataResponse { + public function update(int $fileId, array $data): DataResponse + { try { - $this->authService->authenticateSharedToken($this->request, $fileId); - $user = $this->authService->getAndSetUser($this->request); - $file = $this->fileService->getUserFileById($user->getUID(), $fileId); + $backendSharedToken = $this->request->getHeader('X-Whiteboard-Auth'); + $this->authService->validateBackendSharedToken($backendSharedToken, $fileId); + $userId = $this->request->getHeader('X-Whiteboard-User'); + $user = $this->authService->getWhiteboardUser($userId); + $file = $this->fileService->getFile($user, $fileId); $this->contentService->updateContent($file, $data); return new DataResponse(['status' => 'success']); } catch (Exception $e) { diff --git a/lib/Listener/BeforeTemplateRenderedListener.php b/lib/Listener/BeforeTemplateRenderedListener.php new file mode 100644 index 0000000..ae5948f --- /dev/null +++ b/lib/Listener/BeforeTemplateRenderedListener.php @@ -0,0 +1,35 @@ + */ +class BeforeTemplateRenderedListener implements IEventListener { + public function __construct( + private IInitialState $initialState, + ) { + } + + public function handle(Event $event): void { + if (!($event instanceof BeforeTemplateRenderedEvent)) { + return; + } + + $this->initialState->provideInitialState( + 'file_id', + $event->getShare()->getNodeId() + ); + } +} diff --git a/lib/Listener/RegisterTemplateCreatorListener.php b/lib/Listener/RegisterTemplateCreatorListener.php index 587813a..d971c62 100644 --- a/lib/Listener/RegisterTemplateCreatorListener.php +++ b/lib/Listener/RegisterTemplateCreatorListener.php @@ -25,6 +25,7 @@ public function handle(Event $event): void { if (!($event instanceof RegisterTemplateCreatorEvent)) { return; } + $event->getTemplateManager()->registerTemplateFileCreator(function () { $whiteboard = new TemplateFileCreator(Application::APP_ID, $this->l10n->t('New whiteboard'), '.whiteboard'); diff --git a/lib/Model/AuthenticatedUser.php b/lib/Model/AuthenticatedUser.php new file mode 100644 index 0000000..aac79b9 --- /dev/null +++ b/lib/Model/AuthenticatedUser.php @@ -0,0 +1,24 @@ +user->getUID(); + } + + public function getDisplayName(): string { + return $this->user->getDisplayName(); + } +} diff --git a/lib/Model/PublicSharingUser.php b/lib/Model/PublicSharingUser.php new file mode 100644 index 0000000..8772348 --- /dev/null +++ b/lib/Model/PublicSharingUser.php @@ -0,0 +1,42 @@ +generateRandomUID(); + } + + public function getDisplayName(): string { + return $this->generateRandomDisplayName(); + } + + public function getPublicSharingToken(): string { + return $this->publicSharingToken; + } + + private function generateRandomUID(): string { + return 'shared_' . $this->publicSharingToken . '_' . bin2hex(random_bytes(8)); + } + + private function generateRandomDisplayName(): string { + $adjectives = ['Anonymous', 'Mysterious', 'Incognito', 'Unknown', 'Unnamed']; + $nouns = ['User', 'Visitor', 'Guest', 'Collaborator', 'Participant']; + + $adjective = $adjectives[array_rand($adjectives)]; + $noun = $nouns[array_rand($nouns)]; + + return $adjective . ' ' . $noun; + } +} diff --git a/lib/Model/User.php b/lib/Model/User.php new file mode 100644 index 0000000..501d995 --- /dev/null +++ b/lib/Model/User.php @@ -0,0 +1,15 @@ +getHeader('Authorization'); - if (!$authHeader || sscanf($authHeader, 'Bearer %s', $jwt) !== 1) { - throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED); + public function getAuthenticatedUser(?string $publicSharingToken = null): User { + if ($publicSharingToken) { + return $this->getPublicSharingUser($publicSharingToken); } - if (!is_string($jwt)) { - throw new RuntimeException('JWT token must be a string', Http::STATUS_BAD_REQUEST); + if (!$this->userSession->isLoggedIn()) { + throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED); } - try { - $key = $this->configService->getJwtSecretKey(); - - return JWT::decode($jwt, new Key($key, JWTConsts::JWT_ALGORITHM))->userid; - } catch (Exception) { + $user = $this->userSession->getUser(); + if ($user === null) { throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED); } + + return new AuthenticatedUser($user); } /** * @throws Exception */ - public function getAuthenticatedUser(): IUser { - if (!$this->userSession->isLoggedIn()) { - throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED); - } + private function getPublicSharingUser(string $publicSharingToken): User { + try { + $this->shareManager->getShareByToken($publicSharingToken); - $user = $this->userSession->getUser(); - if ($user === null) { - throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED); + return new PublicSharingUser($publicSharingToken); + } catch (Exception $e) { + throw new RuntimeException('Invalid sharing token', Http::STATUS_UNAUTHORIZED); } - - return $user; } /** * @throws Exception */ - public function authenticateSharedToken(IRequest $request, int $fileId): void { - $whiteboardAuth = $request->getHeader('X-Whiteboard-Auth'); - if (!$whiteboardAuth || !$this->verifySharedToken($whiteboardAuth, $fileId)) { + public function validateBackendSharedToken(string $backendSharedToken, int $fileId): void { + if (!$backendSharedToken || !$this->verifySharedToken($backendSharedToken, $fileId)) { throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED); } } @@ -96,15 +90,22 @@ private function verifySharedToken(string $token, int $fileId): bool { /** * @throws Exception */ - public function getAndSetUser(IRequest $request): IUser { - $whiteboardUser = $request->getHeader('X-Whiteboard-User'); - $user = $this->userManager->get($whiteboardUser); + public function getWhiteboardUser(string $userId): User { + if (str_starts_with($userId, 'shared_')) { + $parts = explode('_', $userId); + if (count($parts) < 3) { + throw new RuntimeException('Invalid user', Http::STATUS_BAD_REQUEST); + } + $sharingToken = $parts[1]; + return new PublicSharingUser($sharingToken); + } + + $user = $this->userManager->get($userId); if (!$user) { throw new RuntimeException('Invalid user', Http::STATUS_BAD_REQUEST); } - $this->userSession->setVolatileActiveUser($user); - return $user; + return new AuthenticatedUser($user); } } diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index e8252e3..566c270 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -10,6 +10,9 @@ namespace OCA\Whiteboard\Service; use OC\User\NoUserException; +use OCA\Whiteboard\Model\AuthenticatedUser; +use OCA\Whiteboard\Model\PublicSharingUser; +use OCA\Whiteboard\Model\User; use OCP\Constants; use OCP\Files\File; use OCP\Files\InvalidPathException; @@ -17,16 +20,34 @@ use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager as ShareManager; /** * @psalm-suppress UndefinedDocblockClass * @psalm-suppress UndefinedClass * @psalm-suppress MissingDependency */ -final class FileService { +final class FileService +{ public function __construct( - private IRootFolder $rootFolder - ) { + private IRootFolder $rootFolder, + private ShareManager $shareManager + ) {} + + /** + * @throws NotFoundException + * @throws NotPermittedException + */ + public function getFile(User $user, int $fileId): File + { + if ($user instanceof AuthenticatedUser) { + return $this->getUserFileById($user->getUID(), $fileId); + } elseif ($user instanceof PublicSharingUser) { + return $this->getFileByShareToken($user->getPublicSharingToken()); + } + + throw new NotFoundException('Invalid user type'); } /** @@ -35,7 +56,8 @@ public function __construct( * @throws NoUserException * @throws InvalidPathException */ - public function getUserFileById(string $userId, int $fileId): File { + public function getUserFileById(string $userId, int $fileId): File + { $userFolder = $this->rootFolder->getUserFolder($userId); $file = $userFolder->getFirstNodeById($fileId); @@ -63,4 +85,24 @@ public function getUserFileById(string $userId, int $fileId): File { return $file; } + + /** + * @throws NotFoundException + */ + public function getFileByShareToken(string $shareToken): File + { + try { + $share = $this->shareManager->getShareByToken($shareToken); + } catch (ShareNotFound $e) { + throw new NotFoundException(); + } + + $node = $share->getNode(); + + if ($node instanceof File) { + return $node; + } + + throw new \InvalidArgumentException('No proper share data'); + } } diff --git a/lib/Service/JWTService.php b/lib/Service/JWTService.php index 8fecbfb..940c589 100644 --- a/lib/Service/JWTService.php +++ b/lib/Service/JWTService.php @@ -9,12 +9,16 @@ namespace OCA\Whiteboard\Service; +use Exception; use Firebase\JWT\JWT; +use Firebase\JWT\Key; use OCA\Whiteboard\Consts\JWTConsts; +use OCA\Whiteboard\Model\User; +use OCP\AppFramework\Http; use OCP\Files\File; use OCP\Files\InvalidPathException; use OCP\Files\NotFoundException; -use OCP\IUser; +use RuntimeException; final class JWTService { public function __construct( @@ -26,13 +30,13 @@ public function __construct( * @throws InvalidPathException * @throws NotFoundException */ - public function generateJWT(IUser $user, File $file, int $fileId): string { + public function generateJWT(User $user, File $file): string { $key = $this->configService->getJwtSecretKey(); $issuedAt = time(); $expirationTime = $issuedAt + JWTConsts::EXPIRATION_TIME; $payload = [ 'userid' => $user->getUID(), - 'fileId' => $fileId, + 'fileId' => $file->getId(), 'permissions' => $file->getPermissions(), 'user' => [ 'id' => $user->getUID(), @@ -44,4 +48,17 @@ public function generateJWT(IUser $user, File $file, int $fileId): string { return JWT::encode($payload, $key, JWTConsts::JWT_ALGORITHM); } + + /** + * @throws Exception + */ + public function getUserIdFromJWT(string $jwt): string { + try { + $key = $this->configService->getJwtSecretKey(); + + return JWT::decode($jwt, new Key($key, JWTConsts::JWT_ALGORITHM))->userid; + } catch (Exception) { + throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED); + } + } } diff --git a/src/App.tsx b/src/App.tsx index 4ba46f8..bd3baca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,9 +32,10 @@ interface WhiteboardAppProps { fileId: number; fileName: string; isEmbedded: boolean; + publicSharingToken: string | null; } -export default function App({ fileId, isEmbedded, fileName }: WhiteboardAppProps) { +export default function App({ fileId, isEmbedded, fileName, publicSharingToken }: WhiteboardAppProps) { const fileNameWithoutExtension = fileName.split('.').slice(0, -1).join('.') const [viewModeEnabled] = useState(isEmbedded) @@ -75,7 +76,7 @@ export default function App({ fileId, isEmbedded, fileName }: WhiteboardAppProps ] = useState(null) const [collab, setCollab] = useState(null) - if (excalidrawAPI && !collab) setCollab(new Collab(excalidrawAPI, fileId)) + if (excalidrawAPI && !collab) setCollab(new Collab(excalidrawAPI, fileId, publicSharingToken)) if (collab && !collab.portal.socket) collab.startCollab() useEffect(() => { const extraTools = document.getElementsByClassName('App-toolbar__extra-tools-trigger')[0] diff --git a/src/collaboration/Portal.ts b/src/collaboration/Portal.ts index 02be2e2..ca33a0d 100644 --- a/src/collaboration/Portal.ts +++ b/src/collaboration/Portal.ts @@ -20,13 +20,13 @@ export class Portal { socket: Socket | null = null roomId: string - roomKey: string collab: Collab + publicSharingToken: string | null - constructor(roomId: string, roomKey: string, collab: Collab) { + constructor(roomId: string, collab: Collab, publicSharingToken: string | null) { this.roomId = roomId - this.roomKey = roomKey this.collab = collab + this.publicSharingToken = publicSharingToken } connectSocket = () => { @@ -42,12 +42,16 @@ export class Portal { auth: { token, }, - transports: ['websocket', 'polling'], + transports: ['websocket'], timeout: 10000, }).connect() socket.on('connect_error', (error) => { - if (error && error.message && !error.message.includes('Authentication error')) { + if ( + error + && error.message + && !error.message.includes('Authentication error') + ) { this.handleConnectionError() } }) @@ -60,7 +64,9 @@ export class Portal { } handleConnectionError = () => { - alert('Failed to connect to the whiteboard server. Redirecting to Files app.') + alert( + 'Failed to connect to the whiteboard server. Redirecting to Files app.', + ) window.location.href = '/index.php/apps/files/files' } @@ -68,7 +74,9 @@ export class Portal { if (this.socket) { this.socket.disconnect() localStorage.removeItem(`jwt-${this.roomId}`) - console.log(`Disconnected from room ${this.roomId} and cleared JWT token`) + console.log( + `Disconnected from room ${this.roomId} and cleared JWT token`, + ) } } @@ -76,23 +84,34 @@ export class Portal { this.socket = socket this.socket?.on('connect_error', async (error) => { - if (error && error.message && error.message.includes('Authentication error')) { + if ( + error + && error.message + && error.message.includes('Authentication error') + ) { await this.handleTokenRefresh() } }) this.socket.on('read-only', () => this.handleReadOnlySocket()) this.socket.on('init-room', () => this.handleInitRoom()) - this.socket.on('room-user-change', (users: { - user: { - id: string, - name: string - }, - socketId: string, - pointer: { x: number, y: number, tool: 'pointer' | 'laser' }, - button: 'down' | 'up', - selectedElementIds: AppState['selectedElementIds'] - }[]) => this.collab.updateCollaborators(users)) - this.socket.on('client-broadcast', (data) => this.handleClientBroadcast(data)) + this.socket.on( + 'room-user-change', + ( + users: { + user: { + id: string + name: string + } + socketId: string + pointer: { x: number; y: number; tool: 'pointer' | 'laser' } + button: 'down' | 'up' + selectedElementIds: AppState['selectedElementIds'] + }[], + ) => this.collab.updateCollaborators(users), + ) + this.socket.on('client-broadcast', (data) => + this.handleClientBroadcast(data), + ) } async handleReadOnlySocket() { @@ -111,7 +130,8 @@ export class Portal { this.socket?.emit('join-room', this.roomId) this.socket?.on('joined-data', (data) => { const remoteElements = JSON.parse(new TextDecoder().decode(data)) - const reconciledElements = this.collab._reconcileElements(remoteElements) + const reconciledElements + = this.collab._reconcileElements(remoteElements) this.collab.handleRemoteSceneUpdate(reconciledElements) this.collab.scrollToContent() }) @@ -136,61 +156,82 @@ export class Portal { async refreshJWT(): Promise { try { - const response = await axios.get(`/index.php/apps/whiteboard/${this.roomId}/token`, { withCredentials: true }) - const token = response.data.token - if (!token) throw new Error('No token received') + let url = `/index.php/apps/whiteboard/${this.roomId}/token` + if (this.publicSharingToken) { + url += `?publicSharingToken=${encodeURIComponent(this.publicSharingToken)}` + } + + const response = await axios.get(url, { withCredentials: true }) + + const token = response.data.token + + console.log('token', token) - localStorage.setItem(`jwt-${this.roomId}`, token) + if (!token) throw new Error('No token received') - return token + localStorage.setItem(`jwt-${this.roomId}`, token) + + return token } catch (error) { - console.error('Error refreshing JWT:', error) - window.location.href = '/index.php/apps/files/files' - return null + console.error('Error refreshing JWT:', error) + window.location.href = '/index.php/apps/files/files' + return null } - } - - async _broadcastSocketData(data: { - type: string; - payload: { - elements?: readonly ExcalidrawElement[]; - socketId?: string; - pointer?: { x: number; y: number; tool: 'pointer' | 'laser' }; - button?: 'down' | 'up'; - selectedElementIds?: AppState['selectedElementIds']; - username?: string; - }; - }, volatile: boolean = false, roomId?: string) { + } + async _broadcastSocketData( + data: { + type: string + payload: { + elements?: readonly ExcalidrawElement[] + socketId?: string + pointer?: { x: number; y: number; tool: 'pointer' | 'laser' } + button?: 'down' | 'up' + selectedElementIds?: AppState['selectedElementIds'] + username?: string + } + }, + volatile: boolean = false, + roomId?: string, + ) { const json = JSON.stringify(data) const encryptedBuffer = new TextEncoder().encode(json) - this.socket?.emit(volatile ? 'server-volatile-broadcast' : 'server-broadcast', roomId ?? this.roomId, encryptedBuffer, []) - + this.socket?.emit( + volatile ? 'server-volatile-broadcast' : 'server-broadcast', + roomId ?? this.roomId, + encryptedBuffer, + [], + ) } - async broadcastScene(updateType: string, elements: readonly ExcalidrawElement[]) { - await this._broadcastSocketData({ type: updateType, payload: { elements } }) + async broadcastScene( + updateType: string, + elements: readonly ExcalidrawElement[], + ) { + await this._broadcastSocketData({ + type: updateType, + payload: { elements }, + }) } async broadcastMouseLocation(payload: { - pointer: { x: number; y: number; tool: 'pointer' | 'laser' }; - button: 'down' | 'up'; - pointersMap: Gesture['pointers']; + pointer: { x: number; y: number; tool: 'pointer' | 'laser' } + button: 'down' | 'up' + pointersMap: Gesture['pointers'] }) { - const data = { type: BroadcastType.MouseLocation, payload: { socketId: this.socket?.id, pointer: payload.pointer, button: payload.button || 'up', - selectedElementIds: this.collab.excalidrawAPI.getAppState().selectedElementIds, + selectedElementIds: + this.collab.excalidrawAPI.getAppState().selectedElementIds, username: this.socket?.id, }, } await this._broadcastSocketData(data, true) - } } diff --git a/src/collaboration/collab.ts b/src/collaboration/collab.ts index bcefdda..4de31a4 100644 --- a/src/collaboration/collab.ts +++ b/src/collaboration/collab.ts @@ -13,13 +13,18 @@ import { hashElementsVersion, reconcileElements } from './util' export class Collab { excalidrawAPI: ExcalidrawImperativeAPI + fileId: number portal: Portal + publicSharingToken: string | null lastBroadcastedOrReceivedSceneVersion: number = -1 private collaborators = new Map() - constructor(excalidrawAPI: ExcalidrawImperativeAPI, fileId: number) { + constructor(excalidrawAPI: ExcalidrawImperativeAPI, fileId: number, publicSharingToken: string | null) { this.excalidrawAPI = excalidrawAPI - this.portal = new Portal(String(fileId), '1', this) + this.fileId = fileId + this.publicSharingToken = publicSharingToken + + this.portal = new Portal(`${fileId}`, this, publicSharingToken) } async startCollab() { diff --git a/src/main.tsx b/src/main.tsx index 942fd83..504bf88 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,72 +4,98 @@ */ import { linkTo } from '@nextcloud/router' -import { StrictMode, lazy } from 'react' +import { StrictMode, lazy, Suspense } from 'react' import { createRoot } from 'react-dom' +import { loadState } from '@nextcloud/initial-state' import './viewer.css' window.EXCALIDRAW_ASSET_PATH = linkTo('whiteboard', 'dist/') -const Component = { - name: 'Whiteboard', - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - render(createElement: (arg0: string, arg1: { attrs: { id: string } }, arg2: string) => any) { - const App = lazy(() => import('./App')) - this.$emit('update:loaded', true) - const randomId = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10) - this.$nextTick(() => { - const rootElement = document.getElementById('whiteboard-' + randomId) - this.root = createRoot(rootElement) +const App = lazy(() => import('./App')) - this.root.render( - - - , - ) +const generateRandomId = () => Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10) + +const renderApp = (rootElement, props) => { + const root = createRoot(rootElement) + root.render( + + Loading...}> + + + , + ) + return root +} + +const publicSharingToken = document.getElementById('sharingToken')?.value || null + +if (publicSharingToken) { + const fileId = loadState('whiteboard', 'file_id') + + document.addEventListener('DOMContentLoaded', () => { + const imgframeElement = document.getElementById('preview') + if (!imgframeElement) { + console.error('#imgframe element not found') + return + } + + imgframeElement.innerHTML = '' + + const whiteboardElement = document.createElement('div') + whiteboardElement.id = `whiteboard-${generateRandomId()}` + whiteboardElement.className = 'whiteboard' + imgframeElement.appendChild(whiteboardElement) + + renderApp(whiteboardElement, { + fileId, + isEmbedded: false, + fileName: document.title, + publicSharingToken, }) - return createElement('div', { - attrs: { - id: 'whiteboard-' + randomId, - }, - class: ['whiteboard', { 'whiteboard-viewer__embedding': this.isEmbedded }], - }, '') - }, - beforeDestroy() { - this.root?.unmount() - }, - props: { - filename: { - type: String, - default: null, + }) +} else { + const Component = { + name: 'Whiteboard', + render(createElement) { + this.$emit('update:loaded', true) + const randomId = generateRandomId() + + this.$nextTick(() => { + const rootElement = document.getElementById(`whiteboard-${randomId}`) + this.root = renderApp(rootElement, { + fileId: this.fileid, + isEmbedded: this.isEmbedded, + fileName: this.basename, + }) + }) + + return createElement('div', { + attrs: { id: `whiteboard-${randomId}` }, + class: ['whiteboard', { 'whiteboard-viewer__embedding': this.isEmbedded }], + }, '') }, - fileid: { - type: Number, - default: null, + beforeDestroy() { + this.root?.unmount() }, - isEmbedded: { - type: Boolean, - default: false, + props: { + filename: { type: String, default: null }, + fileid: { type: Number, default: null }, + isEmbedded: { type: Boolean, default: false }, }, - }, - data() { - return { - root: null, - } - }, -} + data: () => ({ root: null }), + } -if (typeof OCA.Viewer !== 'undefined') { - window.OCA.Viewer.registerHandler({ - id: 'whiteboard', - mimes: [ - 'application/vnd.excalidraw+json', - ], - component: Component, - group: null, - theme: 'default', - canCompare: true, - }) -} else { - alert('UNDEFINED') + if (typeof OCA.Viewer !== 'undefined') { + window.OCA.Viewer.registerHandler({ + id: 'whiteboard', + mimes: ['application/vnd.excalidraw+json'], + component: Component, + group: null, + theme: 'default', + canCompare: true, + }) + } else { + alert('UNDEFINED') + } }