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', 'Zotero') }}
@@ -114,7 +122,7 @@