diff --git a/appinfo/routes.php b/appinfo/routes.php index b86087a2d4..ba06093291 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -36,6 +36,17 @@ ['name' => 'settings#getFontFileOverview', 'url' => 'settings/fonts/{name}/overview', 'verb' => 'GET'], ['name' => 'settings#deleteFontFile', 'url' => 'settings/fonts/{name}', 'verb' => 'DELETE'], ['name' => 'settings#uploadFontFile', 'url' => 'settings/fonts', 'verb' => 'POST'], + [ + 'name' => 'settings#getSettingsFile', + 'url' => 'settings/{type}/{token}/{category}/{name}', + 'verb' => 'GET', + 'requirements' => [ + 'type' => '[a-zA-Z0-9_\-]+', + 'category' => '[a-zA-Z0-9_\-]+', + 'name' => '.+', + ], + ], + ['name' => 'settings#generateIframeToken', 'url' => 'settings/generateToken/{type}', 'verb' => 'GET'], // Direct Editing: Webview ['name' => 'directView#show', 'url' => '/direct/{token}', 'verb' => 'GET'], diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 6dee13062f..bd3a22346d 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -7,11 +7,13 @@ use OCA\Richdocuments\AppConfig; use OCA\Richdocuments\Capabilities; +use OCA\Richdocuments\Db\WopiMapper; use OCA\Richdocuments\Service\CapabilitiesService; use OCA\Richdocuments\Service\ConnectivityService; use OCA\Richdocuments\Service\DemoService; use OCA\Richdocuments\Service\DiscoveryService; use OCA\Richdocuments\Service\FontService; +use OCA\Richdocuments\Service\SettingsService; use OCA\Richdocuments\UploadException; use OCP\App\IAppManager; use OCP\AppFramework\Controller; @@ -54,7 +56,10 @@ public function __construct( private CapabilitiesService $capabilitiesService, private DemoService $demoService, private FontService $fontService, + private SettingsService $settingsService, private LoggerInterface $logger, + private IURLGenerator $urlGenerator, + private WopiMapper $wopiMapper, private ?string $userId, ) { parent::__construct($appName, $request); @@ -96,7 +101,6 @@ public function demoServers(): DataResponse { public function getSettings(): JSONResponse { return new JSONResponse($this->getSettingsData()); } - private function getSettingsData(): array { return [ 'wopi_url' => $this->appConfig->getCollaboraUrlInternal(), @@ -113,6 +117,7 @@ private function getSettingsData(): array { 'esignature_base_url' => $this->appConfig->getAppValue('esignature_base_url'), 'esignature_client_id' => $this->appConfig->getAppValue('esignature_client_id'), 'esignature_secret' => $this->appConfig->getAppValue('esignature_secret'), + 'userId' => $this->userId ]; } @@ -407,6 +412,23 @@ public function getFontFileOverview(string $name): DataDisplayResponse { } } + /** + * @NoAdminRequired + * + * @param string $type - Type is 'admin' or 'user' + * @return DataResponse + */ + public function generateIframeToken(string $type): DataResponse { + try { + $response = $this->settingsService->generateIframeToken($type, $this->userId); + return new DataResponse($response); + } catch (\Exception $e) { + return new DataResponse([ + 'message' => 'Settings token not generated.' + ], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + /** * @param string $name * @return DataResponse @@ -450,6 +472,40 @@ public function uploadFontFile(): JSONResponse { } } + /** + * @param string $type + * @param string $category + * @param string $name + * + * @return DataDisplayResponse + * + * @NoAdminRequired + * @PublicPage + * @NoCSRFRequired + **/ + public function getSettingsFile(string $type, string $token, string $category, string $name) { + try { + $wopi = $this->wopiMapper->getWopiForToken($token); + if ($type === 'userconfig') { + $userId = $wopi->getEditorUid() ?: $wopi->getOwnerUid(); + $type = $type . '/' . $userId; + } + $systemFile = $this->settingsService->getSettingsFile($type, $category, $name); + return new DataDisplayResponse( + $systemFile->getContent(), + 200, + [ + 'Content-Type' => $systemFile->getMimeType() ?: 'application/octet-stream' + ] + ); + } catch (NotFoundException $e) { + return new DataDisplayResponse('File not found.', 404); + } catch (\Exception $e) { + return new DataDisplayResponse('Something went wrong', 500); + } + } + + /** * @param string $key * @return array diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php index 43b5c3bcd7..4ee52e29fb 100644 --- a/lib/Controller/WopiController.php +++ b/lib/Controller/WopiController.php @@ -16,11 +16,14 @@ use OCA\Richdocuments\Exceptions\UnknownTokenException; use OCA\Richdocuments\Helper; use OCA\Richdocuments\PermissionManager; +use OCA\Richdocuments\Service\CapabilitiesService; use OCA\Richdocuments\Service\FederationService; +use OCA\Richdocuments\Service\SettingsService; use OCA\Richdocuments\Service\UserScopeService; use OCA\Richdocuments\TaskProcessingManager; use OCA\Richdocuments\TemplateManager; use OCA\Richdocuments\TokenManager; +use OCA\Richdocuments\WOPI\SettingsUrl; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; @@ -44,6 +47,7 @@ use OCP\Files\Lock\OwnerLockedException; use OCP\Files\Node; use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; use OCP\IConfig; use OCP\IGroupManager; use OCP\IRequest; @@ -86,6 +90,8 @@ public function __construct( private ILockManager $lockManager, private IEventDispatcher $eventDispatcher, private TaskProcessingManager $taskProcessingManager, + private SettingsService $settingsService, + private CapabilitiesService $capabilitiesService, ) { parent::__construct($appName, $request); } @@ -133,11 +139,14 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons } catch (NoLockProviderException|PreConditionNotMetException) { } + $userId = !$isPublic ? $wopi->getEditorUid() : $guestUserId; + + $response = [ 'BaseFileName' => $file->getName(), 'Size' => $file->getSize(), 'Version' => $version, - 'UserId' => !$isPublic ? $wopi->getEditorUid() : $guestUserId, + 'UserId' => $userId, 'OwnerId' => $wopi->getOwnerUid(), 'UserFriendlyName' => $userDisplayName, 'UserExtraInfo' => [], @@ -167,6 +176,14 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons 'ServerPrivateInfo' => [], ]; + if ($this->capabilitiesService->hasSettingIframeSupport()) { + + if (!$isPublic) { + $response['UserSettings'] = $this->generateSettings($userId, 'userconfig'); + } + $response['SharedSettings'] = $this->generateSettings($userId, 'systemconfig'); + } + $enableZotero = $this->config->getAppValue(Application::APPNAME, 'zoteroEnabled', 'yes') === 'yes'; if (!$isPublic && $enableZotero) { $zoteroAPIKey = $this->config->getUserValue($wopi->getEditorUid(), 'richdocuments', 'zoteroAPIKey', ''); @@ -381,6 +398,111 @@ public function getFile(string $fileId, string $access_token): JSONResponse|Stre } } + #[NoAdminRequired] + #[NoCSRFRequired] + #[PublicPage] + #[FrontpageRoute(verb: 'GET', url: 'wopi/settings')] + public function getSettings(string $type, string $access_token): JSONResponse { + if (empty($type)) { + return new JSONResponse(['error' => 'Invalid type parameter'], Http::STATUS_BAD_REQUEST); + } + + try { + $wopi = $this->wopiMapper->getWopiForToken($access_token); + if ($wopi->getTokenType() !== Wopi::TOKEN_TYPE_SETTING_AUTH) { + return new JSONResponse(['error' => 'Invalid token type'], Http::STATUS_BAD_REQUEST); + } + + $isPublic = empty($wopi->getEditorUid()); + $guestUserId = 'Guest-' . \OC::$server->getSecureRandom()->generate(8); + $userId = !$isPublic ? $wopi->getEditorUid() : $guestUserId; + + $userConfig = $this->settingsService->generateSettingsConfig($type, $userId); + return new JSONResponse($userConfig, Http::STATUS_OK); + } catch (UnknownTokenException|ExpiredTokenException $e) { + $this->logger->debug($e->getMessage(), ['exception' => $e]); + return new JSONResponse(['error' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + return new JSONResponse(['error' => 'Internal Server Error'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + #[NoAdminRequired] + #[NoCSRFRequired] + #[PublicPage] + #[FrontpageRoute(verb: 'POST', url: 'wopi/settings/upload')] + public function uploadSettingsFile(string $fileId, string $access_token): JSONResponse { + try { + $wopi = $this->wopiMapper->getWopiForToken($access_token); + + $userId = $wopi->getEditorUid(); + + $content = fopen('php://input', 'rb'); + if (!$content) { + throw new \Exception('Failed to read input stream.'); + } + + $fileContent = stream_get_contents($content); + fclose($content); + + // Use the fileId as a file path URL (e.g., "/settings/systemconfig/wordbook/en_US%20(1).dic") + $settingsUrl = new SettingsUrl($fileId); + $result = $this->settingsService->uploadFile($settingsUrl, $fileContent, $userId); + + return new JSONResponse([ + 'status' => 'success', + 'filename' => $settingsUrl->getFileName(), + 'details' => $result, + ], Http::STATUS_OK); + + } catch (UnknownTokenException $e) { + $this->logger->debug($e->getMessage(), ['exception' => $e]); + return new JSONResponse(['error' => 'Invalid token'], Http::STATUS_FORBIDDEN); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + #[NoAdminRequired] + #[NoCSRFRequired] + #[PublicPage] + #[FrontpageRoute(verb: 'DELETE', url: 'wopi/settings')] + public function deleteSettingsFile(string $fileId, string $access_token): JSONResponse { + try { + $wopi = $this->wopiMapper->getWopiForToken($access_token); + if ($wopi->getTokenType() !== Wopi::TOKEN_TYPE_SETTING_AUTH) { + return new JSONResponse(['error' => 'Invalid token type'], Http::STATUS_FORBIDDEN); + } + + // Parse the dynamic file path from `fileId`, e.g. "/settings/systemconfig/wordbook/en_US (1).dic" + $settingsUrl = new SettingsUrl($fileId); + $type = $settingsUrl->getType(); + $category = $settingsUrl->getCategory(); + $fileName = $settingsUrl->getFileName(); + $userId = $wopi->getEditorUid(); + + $this->settingsService->deleteSettingsFile($type, $category, $fileName, $userId); + + return new JSONResponse([ + 'status' => 'success', + 'message' => "File '$fileName' deleted from '$category' of type '$type'." + ], Http::STATUS_OK); + } catch (UnknownTokenException $e) { + $this->logger->debug($e->getMessage(), ['exception' => $e]); + return new JSONResponse(['error' => 'Invalid token'], Http::STATUS_FORBIDDEN); + } catch (NotFoundException $e) { + return new JSONResponse(['error' => 'File not found'], Http::STATUS_NOT_FOUND); + } catch (NotPermittedException $e) { + return new JSONResponse(['error' => 'Not permitted'], Http::STATUS_FORBIDDEN); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + return new JSONResponse(['error' => 'Internal Server Error'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** * Given an access token and a fileId, replaces the files with the request body. * Expects a valid token in access_token parameter. @@ -863,4 +985,17 @@ private function getWopiUrlForTemplate(Wopi $wopi): string { $nextcloudUrl = $this->appConfig->getNextcloudUrl() ?: trim($this->urlGenerator->getAbsoluteURL(''), '/'); return $nextcloudUrl . '/index.php/apps/richdocuments/wopi/template/' . $wopi->getTemplateId() . '?access_token=' . $wopi->getToken(); } + + private function generateSettingToken(string $userId): string { + return $this->settingsService->generateIframeToken('user', $userId)['token']; + } + + private function generateSettings(string $userId, string $type): array { + $nextcloudUrl = $this->appConfig->getNextcloudUrl() ?: trim($this->urlGenerator->getAbsoluteURL(''), '/'); + $uri = $nextcloudUrl . '/index.php/apps/richdocuments/wopi/settings' . '?type=' . $type . '&access_token=' . $this->generateSettingToken($userId) . '&fileId=' . '-1'; + return [ + 'uri' => $uri, + 'stamp' => time() + ]; + } } diff --git a/lib/Db/Wopi.php b/lib/Db/Wopi.php index 729c8a8cf3..82aaf25ef9 100644 --- a/lib/Db/Wopi.php +++ b/lib/Db/Wopi.php @@ -68,6 +68,11 @@ class Wopi extends Entity implements \JsonSerializable { */ public const TOKEN_TYPE_INITIATOR = 4; + /* + * Temporary token that is used for authentication while communication between cool iframe and user/admin settings + */ + public const TOKEN_TYPE_SETTING_AUTH = 5; + /** @var string */ protected $ownerUid; diff --git a/lib/Db/WopiMapper.php b/lib/Db/WopiMapper.php index ebb472206c..0ed77392a0 100644 --- a/lib/Db/WopiMapper.php +++ b/lib/Db/WopiMapper.php @@ -66,6 +66,28 @@ public function generateFileToken($fileId, $owner, $editor, $version, $updatable return $wopi; } + public function generateUserSettingsToken($fileId, $userId, $version, $serverHost) { + $token = $this->random->generate(32, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS); + + $wopi = Wopi::fromParams([ + 'fileid' => $fileId, + 'ownerUid' => $userId, + 'editorUid' => $userId, + 'version' => $version, + 'canwrite' => true, + 'serverHost' => $serverHost, + 'token' => $token, + 'expiry' => $this->calculateNewTokenExpiry(), + 'templateId' => '0', + 'tokenType' => Wopi::TOKEN_TYPE_SETTING_AUTH, + ]); + + /** @var Wopi $wopi */ + $wopi = $this->insert($wopi); + + return $wopi; + } + public function generateInitiatorToken($uid, $remoteServer) { $token = $this->random->generate(32, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS); diff --git a/lib/Helper.php b/lib/Helper.php index 76272faa86..3e9825a14d 100644 --- a/lib/Helper.php +++ b/lib/Helper.php @@ -40,7 +40,7 @@ public static function parseFileId(string $fileId) { } if (str_contains($fileId, '-')) { - [$fileId, $templateId] = explode('/', $fileId); + [$fileId, $templateId] = array_pad(explode('/', $fileId), 2, null); } return [ diff --git a/lib/Service/CapabilitiesService.php b/lib/Service/CapabilitiesService.php index a97c7a234b..5667e3e493 100644 --- a/lib/Service/CapabilitiesService.php +++ b/lib/Service/CapabilitiesService.php @@ -81,6 +81,10 @@ public function hasZoteroSupport(): bool { return $this->getCapabilities()['hasZoteroSupport'] ?? false; } + public function hasSettingIframeSupport(): bool { + return $this->getCapabilities()['hasSettingIframeSupport'] ?? false; + } + public function hasWASMSupport(): bool { return $this->getCapabilities()['hasWASMSupport'] ?? false; } diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php new file mode 100644 index 0000000000..6bff30f31e --- /dev/null +++ b/lib/Service/SettingsService.php @@ -0,0 +1,323 @@ +cache = $cacheFactory->createDistributed(Application::APPNAME); + } + + /** + * Ensure the settings directory exists, if it doesn't exist then create it. + * + * @param SettingsUrl $settingsUrl + * @return ISimpleFolder + */ + + public function ensureDirectory(SettingsUrl $settingsUrl, string $userId): ISimpleFolder { + $type = $settingsUrl->getType(); + $category = $settingsUrl->getCategory(); + + try { + $baseFolder = $this->appData->getFolder($type); + } catch (NotFoundException $e) { + $baseFolder = $this->appData->newFolder($type); + } + + if ($type === 'userconfig') { + try { + $baseFolder = $baseFolder->getFolder($userId); + } catch (NotFoundException $e) { + $baseFolder = $baseFolder->newFolder($userId); + } + } + + try { + $categoryFolder = $baseFolder->getFolder($category); + } catch (NotFoundException $e) { + $categoryFolder = $baseFolder->newFolder($category); + } + + return $categoryFolder; + } + + /** + * Upload a file to the settings directory. + * ex. $type/$category/$filename + * + * @param SettingsUrl $settingsUrl + * @param string $fileData + * @return array ['stamp' => string, 'uri' => string] + */ + + public function uploadFile(SettingsUrl $settingsUrl, string $fileData, string $userId): array { + $categoryFolder = $this->ensureDirectory($settingsUrl, $userId); + $fileName = $settingsUrl->getFileName(); + $newFile = $categoryFolder->newFile($fileName, $fileData); + $fileUri = $this->generateFileUri($settingsUrl->getType(), $settingsUrl->getCategory(), $fileName, $userId); + + return [ + 'stamp' => $newFile->getETag(), + 'uri' => $fileUri, + ]; + } + + /** + * Get list of files in a setting category. + * + * @param string $type + * @param string $category + * @return array Each item has 'stamp' and 'uri'. + */ + public function getCategoryFileList(string $type, string $category, string $userId): array { + try { + $categoryFolder = $this->appData->getFolder($type . '/' . $category); + } catch (NotFoundException $e) { + return []; + } + + $files = $categoryFolder->getDirectoryListing(); + + return array_map(function (ISimpleFile $file) use ($type, $category, $userId) { + return [ + 'stamp' => $file->getETag(), + 'uri' => $this->generateFileUri($type, $category, $file->getName(), $userId), + ]; + }, $files); + } + + /** + * Get list of files in a setting category. + * + * @param string $type + * @param string $userId + */ + + public function generateIframeToken(string $type, string $userId): array { + try { + if ($type === 'admin' && !$this->groupManager->isAdmin($userId)) { + throw new NotPermittedException('Permission denied'); + } + + $serverHost = $this->urlGenerator->getAbsoluteURL('/'); + $version = $this->capabilitiesService->getProductVersion(); + + $wopi = $this->wopiMapper->generateUserSettingsToken(-1, $userId, $version, $serverHost); + + return [ + 'token' => $wopi->getToken(), + 'token_ttl' => $wopi->getExpiry(), + ]; + } catch (NotPermittedException $e) { + throw $e; + } + } + + /** + * generate setting config + * + * @param string $type + * @return array + */ + public function generateSettingsConfig(string $type, string $userId): array { + $kind = $type === 'userconfig' ? 'user' : 'shared'; + + $config = [ + 'kind' => $kind, + ]; + + if ($type === 'userconfig') { + $type = $type . '/' . $userId; + } + + $categories = $this->getAllCategories($type); + + foreach ($categories as $category) { + $files = $this->getCategoryFileList($type, $category, $userId); + $config[$category] = $files; + } + + return $config; + } + + /** + * Get all setting categories for a setting type. + * + * @param string $type + * @return string[] + */ + private function getAllCategories(string $type): array { + try { + $categories = []; + $directories = $this->getCategoryDirFolderList($type); + foreach ($directories as $dir) { + if ($dir instanceof Folder) { + $categories[] = $dir->getName(); + } + } + return $categories; + } catch (NotFoundException $e) { + return []; + } + } + + /** + * + * @param string $type + * @return Folder[] + */ + private function getCategoryDirFolderList(string $type) : array { + try { + $instanceId = $this->config->getSystemValue('instanceid', null); + if ($instanceId === null) { + throw new NotFoundException('Instance ID not found'); + } + $rootFolder = $this->rootFolder; + $folder = $rootFolder->get('appdata_' . $instanceId . '/richdocuments' . '/' . $type); + if (!$folder instanceof Folder) { + return []; + } + return $folder->getDirectoryListing(); + } catch (NotFoundException $e) { + return []; + } + } + + /** + * Generate file URL. + * + * @param string $type + * @param string $category + * @param string $fileName + * @return string + */ + private function generateFileUri(string $type, string $category, string $fileName, string $userId): string { + + // Passing userId is dangerous so we have to trim from url... + if (strpos($type, '/') !== false) { + $type = explode('/', $type)[0]; + } + + $token = $this->generateIframeToken($type, $userId); + + return $this->urlGenerator->linkToRouteAbsolute( + 'richdocuments.settings.getSettingsFile', + [ + 'type' => $type, + 'token' => $token['token'], + 'category' => $category, + 'name' => $fileName, + ] + ); + } + + /** + * Get a specific settings file. + * + * @param string $type + * @param string $category + * @param string $name + * @return ISimpleFile + */ + public function getSettingsFile(string $type, string $category, string $name): ISimpleFile { + try { + $baseFolder = $this->appData->getFolder($type); + } catch (NotFoundException $e) { + throw new NotFoundException("Type folder '{$type}' not found."); + } + + try { + $categoryFolder = $baseFolder->getFolder($category); + } catch (NotFoundException $e) { + throw new NotFoundException("Category folder '{$category}' not found in type '{$type}'."); + } + + try { + return $categoryFolder->getFile($name); + } catch (NotFoundException $e) { + throw new NotFoundException("File '{$name}' not found in category '{$category}' for type '{$type}'."); + } + } + + /** + * Delete a specific settings file from the type/category directory. + * + * @param string $type + * @param string $category + * @param string $name + */ + public function deleteSettingsFile(string $type, string $category, string $name, string $userId): void { + try { + $baseFolder = $this->appData->getFolder($type); + } catch (NotFoundException $e) { + throw new NotFoundException("Type folder '{$type}' not found."); + } + + if ($type === 'userconfig') { + try { + $baseFolder = $baseFolder->getFolder($userId); + } catch (NotFoundException $e) { + throw new NotFoundException("User folder '{$userId}' not found."); + } + } + + try { + $categoryFolder = $baseFolder->getFolder($category); + } catch (NotFoundException $e) { + throw new NotFoundException("Category folder '{$category}' not found in type '{$type}'."); + } + + try { + if (!$categoryFolder->fileExists($name)) { + throw new NotFoundException("File '{$name}' not found in category '{$category}' for type '{$type}'."); + } + $categoryFolder->getFile($name)->delete(); + } catch (NotFoundException $e) { + throw $e; + } catch (NotPermittedException $e) { + throw $e; + } + } +} diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 7dc7158bb6..708bfa7746 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -56,6 +56,7 @@ public function getForm(): TemplateResponse { 'esignature_base_url' => $this->config->getAppValue('richdocuments', 'esignature_base_url'), 'esignature_client_id' => $this->config->getAppValue('richdocuments', 'esignature_client_id'), 'esignature_secret' => $this->config->getAppValue('richdocuments', 'esignature_secret'), + 'hasSettingIframeSupport' => $this->capabilitiesService->hasSettingIframeSupport(), ], ], 'blank' diff --git a/lib/Settings/Personal.php b/lib/Settings/Personal.php index a6e45228d2..341b12df0b 100644 --- a/lib/Settings/Personal.php +++ b/lib/Settings/Personal.php @@ -6,6 +6,7 @@ namespace OCA\Richdocuments\Settings; +use OCA\Richdocuments\AppConfig; use OCA\Richdocuments\Service\CapabilitiesService; use OCA\Richdocuments\Service\InitialStateService; use OCP\AppFramework\Http\TemplateResponse; @@ -15,6 +16,7 @@ class Personal implements ISettings { public function __construct( private IConfig $config, + private AppConfig $appConfig, private CapabilitiesService $capabilitiesService, private InitialStateService $initialState, private ?string $userId, @@ -39,7 +41,9 @@ public function getForm() { 'documentSigningKey' => $this->config->getUserValue($this->userId, 'richdocuments', 'documentSigningKey', ''), 'documentSigningCa' => $this->config->getUserValue($this->userId, 'richdocuments', 'documentSigningCa', ''), 'hasZoteroSupport' => $this->capabilitiesService->hasZoteroSupport(), - 'zoteroAPIKey' => $this->config->getUserValue($this->userId, 'richdocuments', 'zoteroAPIKey', '') + 'hasSettingIframeSupport' => $this->capabilitiesService->hasSettingIframeSupport(), + 'zoteroAPIKey' => $this->config->getUserValue($this->userId, 'richdocuments', 'zoteroAPIKey', ''), + 'publicWopiUrl' => $this->appConfig->getCollaboraUrlPublic(), ], 'blank' ); diff --git a/lib/WOPI/SettingsUrl.php b/lib/WOPI/SettingsUrl.php new file mode 100644 index 0000000000..1f941b1c9a --- /dev/null +++ b/lib/WOPI/SettingsUrl.php @@ -0,0 +1,103 @@ +rawUrl = $url; + $this->parseUrl($url); + } + + /** + * Factory method to create a SettingsUrl instance based on individual parameters. + */ + public static function fromComponents(string $type, string $category, string $fileName): self { + $rawUrl = "settings/$type/$category/$fileName"; + return new self($rawUrl); + } + + /** + * Parses the settings URL and extracts type, category, and filename. + * + * @param string $url The settings URL to parse. + * @throws InvalidArgumentException If the URL is invalid or incorrectly formatted. + */ + private function parseUrl(string $url): void { + $decodedUrl = urldecode($url); + + $parsedUrl = parse_url($decodedUrl); + if (!isset($parsedUrl['path'])) { + throw new InvalidArgumentException('Invalid URL: Path not found.'); + } + + $path = $parsedUrl['path']; + + $settingsIndex = strpos($path, '/settings/'); + if ($settingsIndex === false) { + throw new InvalidArgumentException("Invalid settings URL format: '/settings/' segment missing."); + } + + $relevantPath = substr($path, $settingsIndex + strlen('/settings/')); + + $pathParts = explode('/', $relevantPath); + + if (count($pathParts) < 3) { + throw new InvalidArgumentException("Invalid settings URL format: Expected 'type/category/fileName'."); + } + + $this->type = $pathParts[0]; + $this->category = $pathParts[1]; + $this->fileName = implode('/', array_slice($pathParts, 2)); + } + + /** + * Get the setting type from the URL. + * + * @return string + */ + public function getType(): string { + return $this->type; + } + + /** + * Get the setting category from the URL. + * + * @return string + */ + public function getCategory(): string { + return $this->category; + } + + /** + * Get the original filename from the URL. + * + * @return string + */ + public function getFileName(): string { + return $this->fileName; + } + + /** + * Get the raw URL. + * + * @return string + */ + public function getRawUrl(): string { + return $this->rawUrl; + } +} diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 18b8167167..fd997336c8 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -217,6 +217,14 @@ + +

{{ t('richdocuments', 'Advanced settings') }}

@@ -553,6 +575,18 @@ export default { } }, }, + async mounted() { + if (this.settings.hasSettingIframeSupport && this.userId && this.userId.length > 0) { + await this.generateAccessToken() + if (this.accessToken) { + this.wopiSettingBaseUrl = getConfigFileUrl() + console.debug('wopiSettingBaseUrl', this.wopiSettingBaseUrl) + this.tokenGenerated = true + } + } else { + console.error('Setting Iframe not supported') + } + }, beforeMount() { for (const key in this.initial.settings) { if (!Object.prototype.hasOwnProperty.call(this.initial.settings, key)) { @@ -574,6 +608,7 @@ export default { Vue.set(this.settings, 'edit_groups', this.settings.edit_groups ? this.settings.edit_groups.split('|') : null) Vue.set(this.settings, 'use_groups', this.settings.use_groups ? this.settings.use_groups.split('|') : null) Vue.set(this.settings, 'fonts', this.initial.fonts ? this.initial.fonts : []) + Vue.set(this.settings, 'hasSettingIframeSupport', this.initial.hasSettingIframeSupport ?? false) this.uiVisible.canonical_webroot = !!(this.settings.canonical_webroot && this.settings.canonical_webroot !== '') this.uiVisible.external_apps = !!(this.settings.external_apps && this.settings.external_apps !== '') @@ -599,6 +634,16 @@ export default { this.checkSettings() }, methods: { + async generateAccessToken() { + const { data } = await axios.get(generateUrl('/apps/richdocuments/settings/generateToken/admin')) + if (data.token) { + this.accessToken = data.token + this.accessTokenTTL = data.token_ttl + console.debug('Admin settings WOPI token generated:', this.accessToken, this.accessTokenTTL) + } else { + console.error('Failed to generate token for admin settings') + } + }, async checkSettings() { this.errorMessage = null this.updating = true @@ -656,6 +701,7 @@ export default { console.error(error) }) }, + async updateUseGroups(enabled) { if (typeof enabled === 'boolean') { this.settings.use_groups = (enabled) ? [] : null diff --git a/src/components/CoolFrame.vue b/src/components/CoolFrame.vue new file mode 100644 index 0000000000..2ef6781e27 --- /dev/null +++ b/src/components/CoolFrame.vue @@ -0,0 +1,90 @@ + +