Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use OCA\Whiteboard\Controller\AiController;
use OCA\Whiteboard\Controller\JWTController;
use OCA\Whiteboard\Controller\PickerController;
use OCA\Whiteboard\Controller\RecordingController;
use OCA\Whiteboard\Controller\SettingsController;
use OCA\Whiteboard\Controller\WhiteboardController;
Expand All @@ -23,6 +24,18 @@
['name' => 'Whiteboard#getLib', 'url' => 'library', 'verb' => 'GET'],
/** @see WhiteboardController::updateLib() */
['name' => 'Whiteboard#updateLib', 'url' => 'library', 'verb' => 'PUT'],
/** @see WhiteboardController::listLibraries() */
['name' => 'Whiteboard#listLibraries', 'url' => 'libraries', 'verb' => 'GET'],
/** @see WhiteboardController::resolveLibrary() */
['name' => 'Whiteboard#resolveLibrary', 'url' => 'libraries/resolve', 'verb' => 'GET'],
/** @see WhiteboardController::saveLibrary() */
['name' => 'Whiteboard#saveLibrary', 'url' => 'libraries', 'verb' => 'POST'],
/** @see WhiteboardController::deleteLibrary() */
['name' => 'Whiteboard#deleteLibrary', 'url' => 'libraries/{scope}/{name}', 'verb' => 'DELETE'],
/** @see WhiteboardController::publishCanvasTemplate() */
['name' => 'Whiteboard#publishCanvasTemplate', 'url' => 'canvas-template', 'verb' => 'POST'],
/** @see PickerController::index() */
['name' => 'Picker#index', 'url' => 'picker', 'verb' => 'GET'],
/** @see WhiteboardController::update() */
['name' => 'Whiteboard#update', 'url' => '{fileId}', 'verb' => 'PUT'],
/** @see WhiteboardController::show() */
Expand All @@ -35,5 +48,17 @@
['name' => 'Settings#update', 'url' => 'settings', 'verb' => 'POST'],
/** @see SettingsController::updatePersonal() */
['name' => 'Settings#updatePersonal', 'url' => 'settings/personal', 'verb' => 'POST'],
/** @see SettingsController::listOrgLibraries() */
['name' => 'Settings#listOrgLibraries', 'url' => 'settings/org-library', 'verb' => 'GET'],
/** @see SettingsController::uploadOrgLibrary() */
['name' => 'Settings#uploadOrgLibrary', 'url' => 'settings/org-library', 'verb' => 'POST'],
/** @see SettingsController::deleteOrgLibrary() */
['name' => 'Settings#deleteOrgLibrary', 'url' => 'settings/org-library/{name}', 'verb' => 'DELETE'],
/** @see SettingsController::listOrgCanvasTemplates() */
['name' => 'Settings#listOrgCanvasTemplates', 'url' => 'settings/org-canvas-template', 'verb' => 'GET'],
/** @see SettingsController::uploadOrgCanvasTemplate() */
['name' => 'Settings#uploadOrgCanvasTemplate', 'url' => 'settings/org-canvas-template', 'verb' => 'POST'],
/** @see SettingsController::deleteOrgCanvasTemplate() */
['name' => 'Settings#deleteOrgCanvasTemplate', 'url' => 'settings/org-canvas-template/{name}', 'verb' => 'DELETE'],
]
];
11 changes: 11 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@

namespace OCA\Whiteboard\AppInfo;

use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
use OCA\Viewer\Event\LoadViewer;
use OCA\Whiteboard\Listener\AddContentSecurityPolicyListener;
use OCA\Whiteboard\Listener\BeforeTemplateRenderedListener;
use OCA\Whiteboard\Listener\FilesLoadAdditionalScriptsListener;
use OCA\Whiteboard\Listener\LoadTextEditorListener;
use OCA\Whiteboard\Listener\LoadViewerListener;
use OCA\Whiteboard\Listener\RegisterDirectEditorListener;
use OCA\Whiteboard\Listener\RegisterTemplateCreatorListener;
use OCA\Whiteboard\Settings\SetupCheck;
use OCA\Whiteboard\Template\GlobalTemplateProvider;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
Expand Down Expand Up @@ -50,6 +53,14 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(RegisterTemplateCreatorEvent::class, RegisterTemplateCreatorListener::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
$context->registerEventListener(RegisterDirectEditorEvent::class, RegisterDirectEditorListener::class);

[$major] = Util::getVersion();
if ($major >= 30) {
$context->registerTemplateProvider(GlobalTemplateProvider::class);
// The picker enhancer only targets the template picker markup that
// ships alongside the template provider API gated above.
$context->registerEventListener(LoadAdditionalScriptsEvent::class, FilesLoadAdditionalScriptsListener::class);
}
$context->registerSetupCheck(SetupCheck::class);
}

Expand Down
102 changes: 102 additions & 0 deletions lib/Controller/PickerController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Whiteboard\Controller;

use OCA\Whiteboard\Service\CanvasTemplateService;
use OCA\Whiteboard\Service\WhiteboardFolderService;
use OCA\Whiteboard\Service\WhiteboardLibraryService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\Files\File;
use OCP\IRequest;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;

/**
* Metadata for the "New whiteboard" template picker: a map of file id to
* {kind, scope} so the Files-app enhancement script can group entries by
* scope and badge them as library or canvas template. Best-effort — every source
* failure is swallowed so the native picker keeps working.
*
* @psalm-suppress UndefinedClass
* @psalm-suppress MissingDependency
* @psalm-suppress UnusedClass
*/
final class PickerController extends Controller {
private const KIND_LIBRARY = 'library';
private const KIND_CANVAS_TEMPLATE = 'canvas-template';

/** Pointer boards are tiny; anything bigger is a real canvas template. */
private const MAX_POINTER_BYTES = 64 * 1024;

public function __construct(
IRequest $request,
private IUserSession $userSession,
private WhiteboardLibraryService $libraryService,
private CanvasTemplateService $canvasTemplateService,
private WhiteboardFolderService $folders,
private LoggerInterface $logger,
) {
parent::__construct('whiteboard', $request);
}

#[NoAdminRequired]
public function index(): DataResponse {
$entries = [];

try {
foreach ($this->libraryService->getOrgLibraryPointerFiles() as $file) {
$entries[(string)$file->getId()] = ['kind' => self::KIND_LIBRARY, 'scope' => WhiteboardFolderService::SCOPE_ORG];
}
} catch (\Throwable $e) {
$this->logger->warning('Failed to list organization library pointers for picker', ['app' => 'whiteboard', 'exception' => $e]);
}

try {
foreach ($this->canvasTemplateService->getOrgCanvasTemplateFiles() as $file) {
$entries[(string)$file->getId()] = ['kind' => self::KIND_CANVAS_TEMPLATE, 'scope' => WhiteboardFolderService::SCOPE_ORG];
}
} catch (\Throwable $e) {
$this->logger->warning('Failed to list organization canvas templates for picker', ['app' => 'whiteboard', 'exception' => $e]);
}

try {
$uid = $this->userSession->getUser()?->getUID();
if ($uid !== null && $uid !== '') {
foreach ($this->folders->getUserTemplateFolder($uid)->getDirectoryListing() as $node) {
if (!$node instanceof File || !str_ends_with(strtolower($node->getName()), '.whiteboard')) {
continue;
}
$entries[(string)$node->getId()] = [
'kind' => $this->isLibraryPointer($node) ? self::KIND_LIBRARY : self::KIND_CANVAS_TEMPLATE,
'scope' => WhiteboardFolderService::SCOPE_PERSONAL,
];
}
}
} catch (\Throwable $e) {
$this->logger->warning('Failed to list personal templates for picker', ['app' => 'whiteboard', 'exception' => $e]);
}

return new DataResponse(['entries' => $entries]);
}

private function isLibraryPointer(File $file): bool {
if ($file->getSize() > self::MAX_POINTER_BYTES) {
return false;
}
try {
$content = json_decode($file->getContent(), true, 16);
} catch (\Throwable) {
return false;
}
return is_array($content) && isset($content['libraryRef']);
}
}
95 changes: 95 additions & 0 deletions lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
namespace OCA\Whiteboard\Controller;

use Exception;
use OCA\Whiteboard\Service\CanvasTemplateService;
use OCA\Whiteboard\Service\ConfigService;
use OCA\Whiteboard\Service\ExceptionService;
use OCA\Whiteboard\Service\JWTService;
use OCA\Whiteboard\Service\WhiteboardLibraryService;
use OCA\Whiteboard\Settings\SetupCheck;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
use OCP\IUserSession;
Expand All @@ -31,6 +34,8 @@ public function __construct(
private ConfigService $configService,
private SetupCheck $setupCheck,
private IUserSession $userSession,
private WhiteboardLibraryService $libraryService,
private CanvasTemplateService $canvasTemplateService,
) {
parent::__construct('whiteboard', $request);
}
Expand Down Expand Up @@ -91,4 +96,94 @@ public function updatePersonal(): DataResponse {
return $this->exceptionService->handleException($e);
}
}

public function listOrgLibraries(): DataResponse {
try {
return new DataResponse(['libraries' => $this->libraryService->listLibraries($this->uid())['org']]);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}
}

public function uploadOrgLibrary(): DataResponse {
try {
$uploadedFile = $this->request->getUploadedFile('file');
if (!is_array($uploadedFile) || !isset($uploadedFile['tmp_name'], $uploadedFile['name'])) {
throw new Exception('No library uploaded', Http::STATUS_BAD_REQUEST);
}
if (($uploadedFile['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
throw new Exception('Library upload failed', Http::STATUS_BAD_REQUEST);
}

$content = file_get_contents($uploadedFile['tmp_name']);
if ($content === false) {
throw new Exception('Failed to read uploaded library', Http::STATUS_BAD_REQUEST);
}

$name = (string)preg_replace('/\.excalidrawlib$/i', '', (string)$uploadedFile['name']);
$items = $this->libraryService->parseLibraryFile($content);

return new DataResponse([
'library' => $this->libraryService->saveLibrary($this->uid(), 'org', $name, $items),
], Http::STATUS_CREATED);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}
}

public function deleteOrgLibrary(string $name): DataResponse {
try {
$this->libraryService->deleteLibrary($this->uid(), 'org', $name);
return new DataResponse(['status' => 'success']);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}
}

public function listOrgCanvasTemplates(): DataResponse {
try {
return new DataResponse(['canvasTemplates' => $this->canvasTemplateService->listOrgCanvasTemplates()]);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}
}

public function uploadOrgCanvasTemplate(): DataResponse {
try {
$uploadedFile = $this->request->getUploadedFile('file');
if (!is_array($uploadedFile) || !isset($uploadedFile['tmp_name'], $uploadedFile['name'])) {
throw new Exception('No canvas uploaded', Http::STATUS_BAD_REQUEST);
}
if (($uploadedFile['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
throw new Exception('Canvas upload failed', Http::STATUS_BAD_REQUEST);
}

$content = file_get_contents($uploadedFile['tmp_name']);
if ($content === false) {
throw new Exception('Failed to read uploaded canvas', Http::STATUS_BAD_REQUEST);
}

$name = (string)preg_replace('/\.whiteboard$/i', '', (string)$uploadedFile['name']);
$data = $this->canvasTemplateService->parseCanvasTemplateData($content);

return new DataResponse([
'canvasTemplate' => $this->canvasTemplateService->publishCanvasTemplate($this->uid(), 'org', $name, $data),
], Http::STATUS_CREATED);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}
}

public function deleteOrgCanvasTemplate(string $name): DataResponse {
try {
$this->canvasTemplateService->deleteOrgCanvasTemplate($name);
return new DataResponse(['status' => 'success']);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}
}

private function uid(): string {
return $this->userSession->getUser()?->getUID() ?? '';
}
}
Loading
Loading