[_version]"
// $parts = explode('_', $docKey);
diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue
index 5aa30220a7..78380f4c10 100644
--- a/src/components/AdminSettings.vue
+++ b/src/components/AdminSettings.vue
@@ -17,7 +17,8 @@
{{ t('richdocuments', 'Collabora Admin Settings') }}
-
@@ -517,6 +518,7 @@ export default {
accessToken: '',
accessTokenTTL: '',
userId: '',
+ tokenGenerated: false,
}
},
computed: {
@@ -630,6 +632,7 @@ export default {
if (data.token) {
this.accessToken = data.token
this.accessTokenTTL = data.token_ttl
+ this.tokenGenerated = true
console.debug('Admin settings WOPI token generated:', this.accessToken, this.accessTokenTTL)
} else if (data.federatedUrl) {
console.error('Federated URL returned, not expected for admin settings.')
diff --git a/src/helpers/url.js b/src/helpers/url.js
index bb63a09e5d..0c4b2115e7 100644
--- a/src/helpers/url.js
+++ b/src/helpers/url.js
@@ -87,7 +87,7 @@ const getNextcloudUrl = () => {
export const getCoolServerUrl = (collaboraBaseUrl) => {
// todo fix wopi Url
- const wopiurl = getCallbackBaseUrl() + '/index.php/apps/richdocuments/wopi/admin-settings'
+ const wopiurl = getCallbackBaseUrl() + '/index.php/apps/richdocuments/wopi/files/-1'
const AdminSettingsUrl = collaboraBaseUrl + '/browser/dist/admin-settings.html?'
From 104c780733e7c79d8ba28a6731887e5af5d09bb4 Mon Sep 17 00:00:00 2001
From: codewithvk
Date: Tue, 7 Jan 2025 22:15:52 +0530
Subject: [PATCH 04/12] WIP: wopi setting upload
Signed-off-by: codewithvk
---
lib/Controller/WopiController.php | 44 +++++++++++++++++++++++++++++++
lib/TokenManager.php | 24 -----------------
2 files changed, 44 insertions(+), 24 deletions(-)
diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php
index 1c232325a7..3c95904831 100644
--- a/lib/Controller/WopiController.php
+++ b/lib/Controller/WopiController.php
@@ -364,6 +364,50 @@ public function getFile(string $fileId, string $access_token): JSONResponse|Stre
}
}
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ #[PublicPage]
+ #[FrontpageRoute(verb: 'POST', url: 'wopi/settings')]
+ public function handleSettingsFile(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);
+ }
+
+ $content = fopen('php://input', 'rb');
+ if (!$content) {
+ throw new \Exception("Failed to read input stream.");
+ }
+
+ $fileContent = stream_get_contents($content);
+ fclose($content);
+
+ if (empty($fileContent)) {
+ throw new \Exception("No file content received.");
+ }
+
+ $jsonContent = json_decode($fileContent, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new \Exception("Invalid JSON content: " . json_last_error_msg());
+ }
+
+ return new JSONResponse($jsonContent, Http::STATUS_OK);
+
+ } catch (UnknownTokenException $e) {
+ $this->logger->debug($e->getMessage(), ['exception' => $e]);
+ return new JSONResponse(['error' => 'Invalid token'], Http::STATUS_FORBIDDEN);
+ } catch (ExpiredTokenException $e) {
+ $this->logger->debug($e->getMessage(), ['exception' => $e]);
+ return new JSONResponse(['error' => 'Token expired'], Http::STATUS_UNAUTHORIZED);
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ return new JSONResponse(['error' => $e->getMessage()], 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.
diff --git a/lib/TokenManager.php b/lib/TokenManager.php
index 2709c10d3e..19b2358671 100644
--- a/lib/TokenManager.php
+++ b/lib/TokenManager.php
@@ -60,30 +60,6 @@ public function generateWopiToken(string $fileId, ?string $shareToken = null, ?s
[$fileId, , $version] = Helper::parseFileId($fileId);
- // // // Parse docKey to extract fileId
- // // // Usually docKey is something like "_[_version]"
- // $parts = explode('_', $docKey);
- // $fileId = (int)$parts[0];
-
- // // If fileId is -1, this is the admin-settings scenario
- // // Skip any file node lookup here and just proceed to generate a token
- // if ($fileId === -1) {
- // // Create a token without referencing a file node
- // // Set fields as needed. No file-related constraints
- // return $this->createToken([
- // 'fileid' => $fileId,
- // 'editor' => $editorUid,
- // 'canwrite' => true, // or false, depending on your needs
- // 'hideDownload' => false,
- // 'direct' => $direct,
- // 'templateId' => null,
- // 'version' => 0,
- // 'server_host' => '', // fill as needed
- // 'ownerUid' => $editorUid,
- // ]);
- // }
-
-
// if the user is not logged-in do use the sharers storage
if ($shareToken !== null) {
/** @var File $file */
From a26cab51bed9907f1e193108f72d0f6b98080976 Mon Sep 17 00:00:00 2001
From: codewithvk
Date: Wed, 8 Jan 2025 23:35:06 +0530
Subject: [PATCH 05/12] Created an AppData-based directory for system settings
and user settings
Signed-off-by: codewithvk
---
composer/composer/autoload_classmap.php | 1 +
composer/composer/autoload_static.php | 1 +
lib/Service/SettingsService.php | 304 ++++++++++++++++++++++++
3 files changed, 306 insertions(+)
create mode 100644 lib/Service/SettingsService.php
diff --git a/composer/composer/autoload_classmap.php b/composer/composer/autoload_classmap.php
index 99384ca436..1bfbe4352b 100644
--- a/composer/composer/autoload_classmap.php
+++ b/composer/composer/autoload_classmap.php
@@ -78,6 +78,7 @@
'OCA\\Richdocuments\\Service\\FederationService' => $baseDir . '/../lib/Service/FederationService.php',
'OCA\\Richdocuments\\Service\\FileTargetService' => $baseDir . '/../lib/Service/FileTargetService.php',
'OCA\\Richdocuments\\Service\\FontService' => $baseDir . '/../lib/Service/FontService.php',
+ 'OCA\\Richdocuments\\Service\\SettingsService' => $baseDir . '/../lib/Service/SettingsService.php',
'OCA\\Richdocuments\\Service\\InitialStateService' => $baseDir . '/../lib/Service/InitialStateService.php',
'OCA\\Richdocuments\\Service\\PdfService' => $baseDir . '/../lib/Service/PdfService.php',
'OCA\\Richdocuments\\Service\\RemoteOptionsService' => $baseDir . '/../lib/Service/RemoteOptionsService.php',
diff --git a/composer/composer/autoload_static.php b/composer/composer/autoload_static.php
index 69c27e4de1..8ca63eda2c 100644
--- a/composer/composer/autoload_static.php
+++ b/composer/composer/autoload_static.php
@@ -111,6 +111,7 @@ class ComposerStaticInitRichdocuments
'OCA\\Richdocuments\\Service\\FederationService' => __DIR__ . '/..' . '/../lib/Service/FederationService.php',
'OCA\\Richdocuments\\Service\\FileTargetService' => __DIR__ . '/..' . '/../lib/Service/FileTargetService.php',
'OCA\\Richdocuments\\Service\\FontService' => __DIR__ . '/..' . '/../lib/Service/FontService.php',
+ 'OCA\\Richdocuments\\Service\\SettingsService' => __DIR__ . '/..' . '/../lib/Service/SettingsService.php',
'OCA\\Richdocuments\\Service\\InitialStateService' => __DIR__ . '/..' . '/../lib/Service/InitialStateService.php',
'OCA\\Richdocuments\\Service\\PdfService' => __DIR__ . '/..' . '/../lib/Service/PdfService.php',
'OCA\\Richdocuments\\Service\\RemoteOptionsService' => __DIR__ . '/..' . '/../lib/Service/RemoteOptionsService.php',
diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php
new file mode 100644
index 0000000000..828189cbe9
--- /dev/null
+++ b/lib/Service/SettingsService.php
@@ -0,0 +1,304 @@
+cache = $cacheFactory->createDistributed(Application::APPNAME);
+ }
+
+ /**
+ * Get or create the system-wide folder in app data.
+ *
+ * @return ISimpleFolder
+ * @throws \OCP\Files\NotPermittedException
+ */
+ private function getSystemDataDir(): ISimpleFolder {
+ try {
+ return $this->appData->getFolder('system-settings');
+ } catch (NotFoundException) {
+ // Folder not found, create it
+ return $this->appData->newFolder('system-settings');
+ }
+ }
+
+ /**
+ * Get or create the user-specific folder in app data.
+ *
+ * Typically you'd pass the user ID, e.g. $this->userSession->getUser()->getUID().
+ *
+ * @param string $userId
+ * @return ISimpleFolder
+ * @throws \OCP\Files\NotPermittedException
+ */
+ private function getUserDataDir(string $userId): ISimpleFolder {
+ try {
+ return $this->appData->getFolder('user-settings-' . $userId);
+ } catch (NotFoundException) {
+ return $this->appData->newFolder('user-settings-' . $userId);
+ }
+ }
+
+ /**
+ * Return a list of file objects from the system-wide directory.
+ *
+ * @return ISimpleFile[]
+ * @throws \OCP\Files\NotPermittedException
+ */
+ public function getSystemFiles(): array {
+ $dir = $this->getSystemDataDir();
+ return $dir->getDirectoryListing();
+ }
+
+ /**
+ * Return a list of file objects from the user-specific directory.
+ *
+ * @param string $userId
+ * @return ISimpleFile[]
+ * @throws \OCP\Files\NotPermittedException
+ */
+ public function getUserFiles(string $userId): array {
+ $dir = $this->getUserDataDir($userId);
+ return $dir->getDirectoryListing();
+ }
+
+ /**
+ * Return a cached list of file names for the system-wide directory.
+ *
+ * @return string[]
+ * @throws \OCP\Files\NotPermittedException
+ */
+ public function getSystemFileNames(): array {
+ $cacheKey = 'systemFileNames';
+ $cachedNames = $this->cache->get($cacheKey);
+
+ if ($cachedNames === null) {
+ $files = $this->getSystemFiles();
+ $cachedNames = array_map(
+ static fn (ISimpleFile $f) => $f->getName(),
+ $files
+ );
+ $this->cache->set($cacheKey, $cachedNames, self::INVALIDATE_FILE_LIST_CACHE_AFTER_SECONDS);
+ }
+
+ return $cachedNames;
+ }
+
+ /**
+ * Return a cached list of file names for a specific user directory.
+ *
+ * @param string $userId
+ * @return string[]
+ * @throws \OCP\Files\NotPermittedException
+ */
+ public function getUserFileNames(string $userId): array {
+ $cacheKey = 'userFileNames_' . $userId;
+ $cachedNames = $this->cache->get($cacheKey);
+
+ if ($cachedNames === null) {
+ $files = $this->getUserFiles($userId);
+ $cachedNames = array_map(
+ static fn (ISimpleFile $f) => $f->getName(),
+ $files
+ );
+ $this->cache->set($cacheKey, $cachedNames, self::INVALIDATE_FILE_LIST_CACHE_AFTER_SECONDS);
+ }
+
+ return $cachedNames;
+ }
+
+ /**
+ * Upload or overwrite a file in the system-wide directory.
+ *
+ * @param string $fileName
+ * @param resource|string $fileData If you have a resource handle or raw string data.
+ * @return array e.g. ['size' => 1234]
+ * @throws \OCP\Files\NotPermittedException
+ */
+ public function uploadSystemFile(string $fileName, $fileData): array {
+ $dir = $this->getSystemDataDir();
+ $newFile = $dir->newFile($fileName, $fileData);
+
+ // Remove cache so it is rebuilt next time
+ $this->cache->remove('systemFileNames');
+
+ return [
+ 'size' => $newFile->getSize(),
+ ];
+ }
+
+ /**
+ * Upload or overwrite a file in the user-specific directory.
+ *
+ * @param string $userId
+ * @param string $fileName
+ * @param resource|string $fileData
+ * @return array
+ * @throws \OCP\Files\NotPermittedException
+ */
+ public function uploadUserFile(string $userId, string $fileName, $fileData): array {
+ $dir = $this->getUserDataDir($userId);
+ $newFile = $dir->newFile($fileName, $fileData);
+
+ // Invalidate cache
+ $this->cache->remove('userFileNames_' . $userId);
+
+ return [
+ 'size' => $newFile->getSize(),
+ ];
+ }
+
+ /**
+ * Get a single file (system-wide).
+ *
+ * @param string $fileName
+ * @return ISimpleFile
+ * @throws NotFoundException
+ * @throws \OCP\Files\NotPermittedException
+ */
+ public function getSystemFile(string $fileName): ISimpleFile {
+ $dir = $this->getSystemDataDir();
+ return $dir->getFile($fileName);
+ }
+
+ /**
+ * Get a single file (user-specific).
+ *
+ * @param string $userId
+ * @param string $fileName
+ * @return ISimpleFile
+ * @throws NotFoundException
+ * @throws \OCP\Files\NotPermittedException
+ */
+ public function getUserFile(string $userId, string $fileName): ISimpleFile {
+ $dir = $this->getUserDataDir($userId);
+ return $dir->getFile($fileName);
+ }
+
+ /**
+ * TODO: need to modify - Return the contents of a system-wide file, for example if it's JSON.
+ *
+ * @param string $fileName
+ * @return string
+ * @throws NotFoundException
+ * @throws \OCP\Files\NotPermittedException
+ */
+ public function getSystemFileContents(string $fileName): string {
+ $file = $this->getSystemFile($fileName);
+ return $file->getContent();
+ }
+
+ /**
+ * TODO: need to modify
+ * Return the contents of a user-specific file.
+ *
+ * @param string $userId
+ * @param string $fileName
+ * @return string
+ * @throws NotFoundException
+ * @throws \OCP\Files\NotPermittedException
+ */
+ public function getUserFileContents(string $userId, string $fileName): string {
+ $file = $this->getUserFile($userId, $fileName);
+ return $file->getContent();
+ }
+
+ /**
+ * Delete a file in the system-wide directory.
+ *
+ * @param string $fileName
+ * @return void
+ * @throws \OCP\Files\NotPermittedException
+ */
+ public function deleteSystemFile(string $fileName): void {
+ $dir = $this->getSystemDataDir();
+ if ($dir->fileExists($fileName)) {
+ $dir->getFile($fileName)->delete();
+ }
+ $this->cache->remove('systemFileNames');
+ }
+
+ /**
+ * Delete a file in the user-specific directory.
+ *
+ * @param string $userId
+ * @param string $fileName
+ * @return void
+ * @throws \OCP\Files\NotPermittedException
+ */
+ public function deleteUserFile(string $userId, string $fileName): void {
+ $dir = $this->getUserDataDir($userId);
+ if ($dir->fileExists($fileName)) {
+ $dir->getFile($fileName)->delete();
+ }
+ $this->cache->remove('userFileNames_' . $userId);
+ }
+
+ /**
+ * Example of a method to bulk-install default files, similar to "installDefaultFonts" in FontService.
+ * This reads from a local assets folder, but adapt as needed.
+ *
+ * @throws Exception
+ */
+ public function installDefaultSystemFiles(): void {
+ $dirPath = __DIR__ . '/../../assets/system-data'; // example location
+
+ if (!is_dir($dirPath)) {
+ throw new Exception("Directory \"$dirPath\" does not exist!");
+ }
+
+ $handle = opendir($dirPath);
+ if (!$handle) {
+ throw new Exception("Failed opening directory \"$dirPath\"!");
+ }
+
+ while (false !== ($fileName = readdir($handle))) {
+ // skip dot-files or any irrelevant files
+ if (str_starts_with($fileName, '.') || str_ends_with($fileName, '.txt')) {
+ continue;
+ }
+
+ $filePath = $dirPath . '/' . $fileName;
+ if (!is_file($filePath)) {
+ continue;
+ }
+
+ // Read file from disk (example)
+ $fileHandle = fopen($filePath, 'r');
+ if (!$fileHandle) {
+ continue;
+ }
+
+ $this->uploadSystemFile($fileName, $fileHandle);
+ }
+ }
+}
From cb707b1b62835f1cb5b464807d07af474989c632 Mon Sep 17 00:00:00 2001
From: codewithvk
Date: Wed, 8 Jan 2025 23:40:04 +0530
Subject: [PATCH 06/12] Create a Settings controller API endpoint for handling
AppData-based directories
Signed-off-by: codewithvk
---
appinfo/routes.php | 6 +
lib/Controller/SettingsController.php | 165 ++++++++++++++++++++++++++
2 files changed, 171 insertions(+)
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 92d1b6cca4..b9fda2455e 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -37,6 +37,12 @@
['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#uploadSystemFile', 'url' => 'settings/system-files', 'verb' => 'POST'],
+ ['name' => 'settings#getSystemFileList', 'url' => 'settings/system-files.json', 'verb' => 'GET' ],
+ ['name' => 'settings#getSystemFile', 'url' => 'settings/system-files/{fileName}', 'verb' => 'GET'],
+ ['name' => 'settings#uploadUserFile', 'url' => 'settings/user-files', 'verb' => 'POST'],
+ ['name' => 'settings#getUserFileList', 'url' => 'settings/user-files.json', 'verb' => 'GET'],
+ ['name' => 'settings#downloadUserFile', 'url' => 'settings/user-files/{fileName}', '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 ae6aba90fa..740fb0117b 100644
--- a/lib/Controller/SettingsController.php
+++ b/lib/Controller/SettingsController.php
@@ -7,6 +7,7 @@
use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\Capabilities;
+use OCA\Richdocuments\Service\SettingsService;
use OCA\Richdocuments\Service\CapabilitiesService;
use OCA\Richdocuments\Service\ConnectivityService;
use OCA\Richdocuments\Service\DemoService;
@@ -54,6 +55,7 @@ public function __construct(
private CapabilitiesService $capabilitiesService,
private DemoService $demoService,
private FontService $fontService,
+ private SettingsService $settingsService,
private LoggerInterface $logger,
private ?string $userId,
) {
@@ -433,6 +435,169 @@ public function uploadFontFile(): JSONResponse {
}
}
+ /**
+ * @return JSONResponse
+ * @throws UploadException
+ * @throws NotPermittedException
+ * @throws Exception
+ */
+ public function uploadSystemFile(): JSONResponse {
+ try {
+ $file = $this->getUploadedFile('systemfile');
+ if (!isset($file['tmp_name'], $file['name'])) {
+ return new JSONResponse(['error' => 'No uploaded file'], 400);
+ }
+
+ $newFileResource = fopen($file['tmp_name'], 'rb');
+ if ($newFileResource === false) {
+ throw new UploadException('Could not open file resource');
+ }
+
+ $result = $this->settingsService->uploadSystemFile($file['name'], $newFileResource);
+ return new JSONResponse($result);
+ } catch (NotPermittedException $e) {
+ $this->logger->error('Not permitted', ['exception' => $e]);
+ return new JSONResponse(['error' => 'Not permitted'], 403);
+ } catch (UploadException $e) {
+ $this->logger->error('UploadException', ['exception' => $e]);
+ return new JSONResponse(['error' => $e->getMessage()], 400);
+ } catch (\Exception $e) {
+ $this->logger->error('General error', ['exception' => $e]);
+ return new JSONResponse(['error' => $e->getMessage()], 500);
+ }
+ }
+
+ /**
+ * @return JSONResponse
+ */
+ public function getSystemFileList(): JSONResponse {
+ try {
+ $fileNames = $this->settingsService->getSystemFileNames();
+ return new JSONResponse($fileNames);
+ } catch (NotPermittedException $e) {
+ return new JSONResponse(['error' => 'Not permitted'], Http::STATUS_FORBIDDEN);
+ }
+ }
+
+ /**
+ * @param string $fileName
+ * @return DataResponse
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @PublicPage
+ *
+ */
+ public function getSystemFile(string $fileName) {
+ try {
+ $systemFile = $this->settingsService->getSystemFile($fileName);
+ $mimeType = $systemFile->getMimeType() ?: 'application/octet-stream';
+ $fileContents = $systemFile->getContent();
+
+ return new DataDisplayResponse(
+ $fileContents,
+ Http::STATUS_OK,
+ [
+ 'Content-Type' => $mimeType,
+ 'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
+ ]
+ );
+ } 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);
+ }
+ }
+
+ /**
+ * @return JSONResponse
+ */
+ public function uploadUserFile(): JSONResponse {
+ // Make sure we know who is uploading
+ if ($this->userId === null) {
+ return new JSONResponse(['error' => 'User not logged in'], 401);
+ }
+
+ try {
+ // The key "userfile" must match the FormData append() key in Vue
+ $file = $this->getUploadedFile('userfile');
+ if (!isset($file['tmp_name'], $file['name'])) {
+ return new JSONResponse(['error' => 'No uploaded file'], 400);
+ }
+
+ $newFileResource = fopen($file['tmp_name'], 'rb');
+ if ($newFileResource === false) {
+ throw new UploadException('Could not open file resource');
+ }
+
+ $result = $this->settingsService->uploadUserFile($this->userId, $file['name'], $newFileResource);
+ return new JSONResponse($result); // e.g. { "size": 1234 }
+ } catch (NotPermittedException $e) {
+ $this->logger->error('Not permitted', ['exception' => $e]);
+ return new JSONResponse(['error' => 'Not permitted'], 403);
+ } catch (UploadException $e) {
+ $this->logger->error('UploadException', ['exception' => $e]);
+ return new JSONResponse(['error' => $e->getMessage()], 400);
+ } catch (\Exception $e) {
+ $this->logger->error('General error', ['exception' => $e]);
+ return new JSONResponse(['error' => $e->getMessage()], 500);
+ }
+ }
+
+ /**
+ * @return JSONResponse
+ */
+ public function getUserFileList(): JSONResponse {
+ if ($this->userId === null) {
+ return new JSONResponse(['error' => 'User not logged in'], Http::STATUS_UNAUTHORIZED);
+ }
+
+ try {
+ $fileNames = $this->settingsService->getUserFileNames($this->userId);
+ return new JSONResponse($fileNames);
+ } catch (NotPermittedException $e) {
+ return new JSONResponse(['error' => 'Not permitted'], Http::STATUS_FORBIDDEN);
+ } catch (\Exception $e) {
+ return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ /**
+ * @param string $fileName
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ */
+ public function downloadUserFile(string $fileName) {
+ if ($this->userId === null) {
+ return new JSONResponse(['error' => 'User not logged in'], Http::STATUS_UNAUTHORIZED);
+ }
+
+ try {
+ $userFile = $this->settingsService->getUserFile($this->userId, $fileName);
+ $mimeType = $userFile->getMimeType() ?: 'application/octet-stream';
+ $content = $userFile->getContent(); // get file bytes
+
+ // Return as DataDisplayResponse
+ $response = new DataDisplayResponse(
+ $content,
+ Http::STATUS_OK,
+ [
+ 'Content-Type' => $mimeType,
+ 'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
+ ]
+ );
+
+ return $response;
+ } 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) {
+ return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+ }
+
/**
* @param string $key
* @return array
From 4a62c88ff969048b6e12739f5c59fd72888138e5 Mon Sep 17 00:00:00 2001
From: codewithvk
Date: Wed, 8 Jan 2025 23:43:02 +0530
Subject: [PATCH 07/12] Temporary Commit: Created a temporary UI to validate
the functionality of user and system settings.
Note: We will delete this commit/code after the entire prototype is ready. Reviewers can ignore this commit during the review process.
Signed-off-by: codewithvk
---
src/components/AdminSettings.vue | 160 ++++++++++++++++++++++++++++++-
1 file changed, 159 insertions(+), 1 deletion(-)
diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue
index 78380f4c10..2b15d4971d 100644
--- a/src/components/AdminSettings.vue
+++ b/src/components/AdminSettings.vue
@@ -24,6 +24,63 @@
:access-token-t-t-l="accessTokenTTL" />
+
+
{{ t('richdocuments', 'Upload System File (Minimal Example)') }}
+
{{ t('richdocuments', 'Pick a file to upload into the system-settings folder:') }}
+
+
+
+
+
+
System Files
+
+
+
+
+
+
+
Upload File to User Settings (Minimal Example)
+
Select a file to store in "user-settings-{{ userId }}" folder:
+
+
+
+
+
+
User Files
+
Load and see your user-specific files stored in user-settings-{{ userId }}.
+
+
+
+
+
+
{{ t('richdocuments', 'Could not establish connection to the Collabora Online server.') }}
@@ -471,6 +528,12 @@ export default {
productName: loadState('richdocuments', 'productName', 'Nextcloud Office'),
hasNextcloudBranding: loadState('richdocuments', 'hasNextcloudBranding', true),
+ uploadingSystemFile: false,
+ systemFiles: [],
+ uploadingUserFile: false,
+ userFiles: [],
+ loadingUserFiles: false,
+
serverMode: '',
serverError: SERVER_STATE_LOADING,
errorMessage: null,
@@ -571,6 +634,13 @@ export default {
} else {
console.error('User not authenticated')
}
+ if (this.isSetup) {
+ this.getSystemFiles()
+ }
+
+ if (this.isSetup) {
+ this.getUserFiles()
+ }
},
beforeMount() {
for (const key in this.initial.settings) {
@@ -632,7 +702,7 @@ export default {
if (data.token) {
this.accessToken = data.token
this.accessTokenTTL = data.token_ttl
- this.tokenGenerated = true
+ this.tokenGenerated = true
console.debug('Admin settings WOPI token generated:', this.accessToken, this.accessTokenTTL)
} else if (data.federatedUrl) {
console.error('Federated URL returned, not expected for admin settings.')
@@ -697,6 +767,94 @@ export default {
console.error(error)
})
},
+ uploadSystemFile(event) {
+ const file = event.target.files[0]
+ if (!file) {
+ return
+ }
+ this.uploadingSystemFile = true
+
+ const formData = new FormData()
+ formData.append('systemfile', file)
+
+ const url = this.$options.methods.generateSystemFilesUrl()
+
+ axios.post(url, formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ }).then((response) => {
+ }).catch((error) => {
+ console.error('System file upload error:', error)
+ showError(error?.response?.data?.error ?? 'Unknown error')
+ }).finally(() => {
+ this.uploadingSystemFile = false
+ event.target.value = ''
+ })
+ },
+
+ generateSystemFilesUrl() {
+ return generateUrl('/apps/richdocuments/settings/system-files')
+ },
+
+ getSystemFiles() {
+ const url = generateUrl('/apps/richdocuments/settings/system-files.json')
+ axios.get(url)
+ .then((response) => {
+ this.systemFiles = response.data
+ })
+ .catch((error) => {
+ console.error('Failed to load system files:', error)
+ })
+ },
+
+ downloadUrl(fileName) {
+ return generateUrl('/apps/richdocuments/settings/system-files/' + encodeURIComponent(fileName))
+ },
+
+ uploadUserFile(event) {
+ const file = event.target.files[0]
+ if (!file) {
+ return
+ }
+ this.uploadingUserFile = true
+
+ const formData = new FormData()
+ formData.append('userfile', file)
+
+ const url = generateUrl('/apps/richdocuments/settings/user-files')
+ axios.post(url, formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ })
+ .then((response) => {
+ })
+ .catch((error) => {
+ console.error('User file upload error:', error)
+ })
+ .finally(() => {
+ this.uploadingUserFile = false
+ event.target.value = ''
+ })
+ },
+
+ getUserFiles() {
+ this.loadingUserFiles = true
+ const url = generateUrl('/apps/richdocuments/settings/user-files.json')
+ axios.get(url)
+ .then((response) => {
+ this.userFiles = response.data
+ })
+ .catch((error) => {
+ console.error('Failed to load user files:', error)
+ showError('Could not load your user files')
+ })
+ .finally(() => {
+ this.loadingUserFiles = false
+ })
+ },
+
+ downloadUserFileUrl(fileName) {
+ return generateUrl('/apps/richdocuments/settings/user-files/' + encodeURIComponent(fileName))
+ },
+
async updateUseGroups(enabled) {
if (typeof enabled === 'boolean') {
this.settings.use_groups = (enabled) ? [] : null
From d70c46639a94336465e051cc4e77c281c626e121 Mon Sep 17 00:00:00 2001
From: codewithvk
Date: Fri, 10 Jan 2025 00:37:43 +0530
Subject: [PATCH 08/12] Change the admin settings iframe URL to
adminIntegratorSettings.
TODO: We should try to centralize the path everywhere so that a change in one place updates it everywhere!
Signed-off-by: codewithvk
---
lib/Controller/DocumentController.php | 4 ++--
src/components/AdminSettings.vue | 4 ++--
src/helpers/url.js | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/lib/Controller/DocumentController.php b/lib/Controller/DocumentController.php
index 50135829de..5d361ce73b 100644
--- a/lib/Controller/DocumentController.php
+++ b/lib/Controller/DocumentController.php
@@ -384,7 +384,7 @@ public function editOnlineTarget(int $fileId, ?string $target = null): RedirectR
#[PublicPage]
public function token(int $fileId, ?string $shareToken = null, ?string $path = null, ?string $guestName = null): DataResponse {
try {
- if ($fileId === -1 && $path !== null && str_starts_with($path, 'admin-settings/')) {
+ if ($fileId === -1 && $path !== null && str_starts_with($path, 'adminIntegratorSettings/')) {
$parts = explode('/', $path);
$adminUserId = $parts[1] ?? $this->userId; // fallback if needed
@@ -393,7 +393,7 @@ public function token(int $fileId, ?string $shareToken = null, ?string $path = n
$wopi = $this->tokenManager->generateWopiToken($fileId, null, $adminUserId);
$coolBaseUrl = $this->appConfig->getCollaboraUrlPublic();
- $adminSettingsWopiSrc = $coolBaseUrl . '/browser/admin-settings.html?';
+ $adminSettingsWopiSrc = $coolBaseUrl . '/browser/adminIntegratorSettings.html?';
return new DataResponse([
'urlSrc' => $adminSettingsWopiSrc,
diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue
index 2b15d4971d..27b35bab66 100644
--- a/src/components/AdminSettings.vue
+++ b/src/components/AdminSettings.vue
@@ -18,7 +18,7 @@
{{ t('richdocuments', 'Collabora Admin Settings') }}
@@ -690,7 +690,7 @@ export default {
methods: {
async generateAccessToken() {
const fileId = -1
- const path = `admin-settings/${this.userId}`
+ const path = `adminIntegratorSettings/${this.userId}`
const guestName = this.userId
const { data } = await axios.post(generateUrl('/apps/richdocuments/token'), {
diff --git a/src/helpers/url.js b/src/helpers/url.js
index 0c4b2115e7..1c87a74d6b 100644
--- a/src/helpers/url.js
+++ b/src/helpers/url.js
@@ -89,7 +89,7 @@ export const getCoolServerUrl = (collaboraBaseUrl) => {
// todo fix wopi Url
const wopiurl = getCallbackBaseUrl() + '/index.php/apps/richdocuments/wopi/files/-1'
- const AdminSettingsUrl = collaboraBaseUrl + '/browser/dist/admin-settings.html?'
+ const AdminSettingsUrl = collaboraBaseUrl + '/browser/dist/admin/adminIntegratorSettings.html?'
return AdminSettingsUrl
+ 'WOPISrc=' + encodeURIComponent(wopiurl)
From ce2ccba4ba8443a9d24ae356ed5059189a752862 Mon Sep 17 00:00:00 2001
From: codewithvk
Date: Fri, 10 Jan 2025 00:40:35 +0530
Subject: [PATCH 09/12] WOPI: Update the wopi setting upload route to accept a
file and store it to system-settings dir
Signed-off-by: codewithvk
---
lib/Controller/WopiController.php | 27 +++++++++++++--------------
1 file changed, 13 insertions(+), 14 deletions(-)
diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php
index 3c95904831..efdb723d01 100644
--- a/lib/Controller/WopiController.php
+++ b/lib/Controller/WopiController.php
@@ -56,6 +56,7 @@
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
+use OCA\Richdocuments\Service\SettingsService;
#[RestrictToWopiServer]
class WopiController extends Controller {
@@ -84,6 +85,7 @@ public function __construct(
private IGroupManager $groupManager,
private ILockManager $lockManager,
private IEventDispatcher $eventDispatcher,
+ private SettingsService $settingsService,
) {
parent::__construct($appName, $request);
}
@@ -367,7 +369,7 @@ public function getFile(string $fileId, string $access_token): JSONResponse|Stre
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
- #[FrontpageRoute(verb: 'POST', url: 'wopi/settings')]
+ #[FrontpageRoute(verb: 'POST', url: 'wopi/settings/upload')]
public function handleSettingsFile(string $access_token): JSONResponse {
try {
$wopi = $this->wopiMapper->getWopiForToken($access_token);
@@ -380,34 +382,31 @@ public function handleSettingsFile(string $access_token): JSONResponse {
if (!$content) {
throw new \Exception("Failed to read input stream.");
}
-
+
$fileContent = stream_get_contents($content);
fclose($content);
- if (empty($fileContent)) {
- throw new \Exception("No file content received.");
- }
- $jsonContent = json_decode($fileContent, true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \Exception("Invalid JSON content: " . json_last_error_msg());
- }
+ $newFileName = 'settings-' . uniqid() . '.json';
+
+ $result = $this->settingsService->uploadSystemFile($newFileName, $fileContent);
- return new JSONResponse($jsonContent, Http::STATUS_OK);
+ return new JSONResponse([
+ 'status' => 'success',
+ 'filename' => $newFileName,
+ '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 (ExpiredTokenException $e) {
- $this->logger->debug($e->getMessage(), ['exception' => $e]);
- return new JSONResponse(['error' => 'Token expired'], Http::STATUS_UNAUTHORIZED);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return new JSONResponse(['error' => $e->getMessage()], 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.
From e1f8021979ee8c178a048f616c60313a8b0744ee Mon Sep 17 00:00:00 2001
From: codewithvk
Date: Sat, 11 Jan 2025 12:39:22 +0530
Subject: [PATCH 10/12] add delete button and create wopi/setting route to
handle wopi file request
Signed-off-by: codewithvk
---
appinfo/routes.php | 1 +
lib/Controller/SettingsController.php | 25 ++++++++++++
lib/Controller/WopiController.php | 55 ++++++++++++++++++++++++---
lib/Service/SettingsService.php | 30 +++++++++++++++
src/components/AdminSettings.vue | 22 +++++++++++
5 files changed, 127 insertions(+), 6 deletions(-)
diff --git a/appinfo/routes.php b/appinfo/routes.php
index b9fda2455e..8387cb9d6d 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -43,6 +43,7 @@
['name' => 'settings#uploadUserFile', 'url' => 'settings/user-files', 'verb' => 'POST'],
['name' => 'settings#getUserFileList', 'url' => 'settings/user-files.json', 'verb' => 'GET'],
['name' => 'settings#downloadUserFile', 'url' => 'settings/user-files/{fileName}', 'verb' => 'GET'],
+ ['name' => 'settings#deleteSystemFile', 'url' => 'settings/system-files/{fileName}', 'verb' => 'DELETE'],
// Direct Editing: Webview
['name' => 'directView#show', 'url' => '/direct/{token}', 'verb' => 'GET'],
diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php
index 740fb0117b..56406912bd 100644
--- a/lib/Controller/SettingsController.php
+++ b/lib/Controller/SettingsController.php
@@ -508,6 +508,31 @@ public function getSystemFile(string $fileName) {
return new JSONResponse(['error' => 'Not permitted'], Http::STATUS_FORBIDDEN);
}
}
+
+ /**
+ * Delete a file by name from the "system-settings" directory.
+ *
+ * @NoAdminRequired
+ * @PublicPage
+ * @NoCSRFRequired
+ *
+ * @param string $fileName Name of the file to delete (URL-encoded)
+ * @return JSONResponse A simple JSON indicating success or error
+ */
+ public function deleteSystemFile(string $fileName): JSONResponse {
+ try {
+ $this->settingsService->deleteSystemFile($fileName);
+ return new JSONResponse(['status' => 'success'], Http::STATUS_OK);
+ } 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' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+ }
+
/**
* @return JSONResponse
diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php
index efdb723d01..57ddb4619e 100644
--- a/lib/Controller/WopiController.php
+++ b/lib/Controller/WopiController.php
@@ -103,9 +103,11 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons
// TODO: condition for $wopi not found?
+ $userSettingsUri = $this->generateUserSettingsUri($wopi);
+
if ($fileId == "-1" && $wopi->getTokenType() == WOPI::TOKEN_TYPE_SETTING_AUTH) {
$response = [
- "usersettings" => 'DONE',
+ "UserSettingsUri" => $userSettingsUri,
];
return new JSONResponse($response);
@@ -171,6 +173,7 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons
'IsUserLocked' => $this->permissionManager->userIsFeatureLocked($wopi->getEditorUid()),
'EnableRemoteLinkPicker' => (bool)$wopi->getCanwrite() && !$isPublic && !$wopi->getDirect(),
'HasContentRange' => true,
+ "UserSettingsUri" => $userSettingsUri,
];
$enableZotero = $this->config->getAppValue(Application::APPNAME, 'zoteroEnabled', 'yes') === 'yes';
@@ -366,11 +369,48 @@ 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 ($type !== 'UserSettingsUri') {
+ 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_FORBIDDEN);
+ }
+
+ // user admin check
+ $user = $this->userManager->get($wopi->getEditorUid());
+ if (!$user || !$this->groupManager->isAdmin($user->getUID())) {
+ return new JSONResponse(['error' => 'Access denied'], Http::STATUS_FORBIDDEN);
+ }
+
+ $systemFiles = $this->settingsService->getSystemFiles();
+ $formattedList = $this->settingsService->getSystemFileList($systemFiles);
+
+ $response = new JSONResponse($formattedList);
+
+ return $response;
+ } 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 handleSettingsFile(string $access_token): JSONResponse {
+ public function handleSettingsFile(string $fileId, string $access_token): JSONResponse {
try {
$wopi = $this->wopiMapper->getWopiForToken($access_token);
@@ -386,10 +426,8 @@ public function handleSettingsFile(string $access_token): JSONResponse {
$fileContent = stream_get_contents($content);
fclose($content);
-
- $newFileName = 'settings-' . uniqid() . '.json';
-
- $result = $this->settingsService->uploadSystemFile($newFileName, $fileContent);
+ // TODO: JSON based upload
+ $result = $this->settingsService->uploadSystemFile($fileId, $fileContent);
return new JSONResponse([
'status' => 'success',
@@ -900,4 +938,9 @@ 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();
}
+ // todo extract nextcloud url from everything
+ private function generateUserSettingsUri(Wopi $wopi): string {
+ $nextcloudUrl = $this->appConfig->getNextcloudUrl() ?: trim($this->urlGenerator->getAbsoluteURL(''), '/');
+ return $nextcloudUrl . '/index.php/apps/richdocuments/wopi/settings' . '?type=UserSettingsUri' . '&access_token=' . $wopi->getToken();
+ }
}
diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php
index 828189cbe9..35a7fd78dc 100644
--- a/lib/Service/SettingsService.php
+++ b/lib/Service/SettingsService.php
@@ -79,6 +79,36 @@ public function getSystemFiles(): array {
return $dir->getDirectoryListing();
}
+ /**
+ * Get the formatted list of system-settings files.
+ *
+ * @param array $systemFiles Array of ISimpleFile objects
+ * @return array
+ */
+ public function getSystemFileList(array $systemFiles): array {
+ $urlGenerator = $this->urlGenerator;
+ $list = array_map(
+ function (ISimpleFile $f) use ($urlGenerator) {
+ return [
+ 'uri' => $urlGenerator->linkToRouteAbsolute(
+ Application::APPNAME . '.settings.getSystemFile',
+ ['fileName' => $f->getName()]
+ ),
+ 'stamp' => $f->getETag(),
+ ];
+ },
+ $systemFiles
+ );
+
+ // Combine all ETags into a single ETag for the entire list
+ $combinedEtag = md5(implode(',', array_map(fn(ISimpleFile $f) => $f->getETag(), $systemFiles)));
+
+ return [
+ 'files' => $list,
+ 'etag' => $combinedEtag,
+ ];
+ }
+
/**
* Return a list of file objects from the user-specific directory.
*
diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue
index 27b35bab66..a82463122f 100644
--- a/src/components/AdminSettings.vue
+++ b/src/components/AdminSettings.vue
@@ -48,6 +48,11 @@
class="button">
{{ t('richdocuments', 'Download') }}
+
+
@@ -806,6 +811,23 @@ export default {
})
},
+ deleteSystemFile(fileName) {
+ if (!window.confirm(`Are you sure you want to delete "${fileName}"?`)) {
+ return
+ }
+
+ const url = generateUrl('/apps/richdocuments/settings/system-files/' + encodeURIComponent(fileName))
+
+ axios.delete(url)
+ .then((response) => {
+ this.getSystemFiles()
+ })
+ .catch((error) => {
+ console.error('Delete error', error.response?.data || error)
+ showError(t('richdocuments', 'Could not delete file') + ': ' + fileName)
+ })
+ },
+
downloadUrl(fileName) {
return generateUrl('/apps/richdocuments/settings/system-files/' + encodeURIComponent(fileName))
},
From cc91ef8a8b6b316ec30d4a6d70bb29087ed86d07 Mon Sep 17 00:00:00 2001
From: codewithvk
Date: Sat, 11 Jan 2025 23:49:59 +0530
Subject: [PATCH 11/12] Manage setting configs files with dynamic routes
- Implement dynamic routing for settings files, enabling URLs structured as /settings/{type}/{category}/{filename}.
- Support various setting types (e.g. userconfigs, sharedconfigs) and categories (e.g. autotext, wordbook) so that multiple files can be stored for each category.
- Ensure proper URL parsing and directory handling for uploading and retrieving files via the WOPI interface.
Signed-off-by: codewithvk
---
appinfo/routes.php | 10 ++
composer/composer/autoload_classmap.php | 1 +
composer/composer/autoload_static.php | 1 +
lib/Controller/SettingsController.php | 30 +++++
lib/Controller/WopiController.php | 28 ++--
lib/Service/SettingsService.php | 165 ++++++++++++++++++++++++
lib/WOPI/SettingsUrl.php | 99 ++++++++++++++
tests/psalm-baseline.xml | 5 +
8 files changed, 323 insertions(+), 16 deletions(-)
create mode 100644 lib/WOPI/SettingsUrl.php
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 8387cb9d6d..3965c11013 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -44,6 +44,16 @@
['name' => 'settings#getUserFileList', 'url' => 'settings/user-files.json', 'verb' => 'GET'],
['name' => 'settings#downloadUserFile', 'url' => 'settings/user-files/{fileName}', 'verb' => 'GET'],
['name' => 'settings#deleteSystemFile', 'url' => 'settings/system-files/{fileName}', 'verb' => 'DELETE'],
+ [
+ 'name' => 'settings#getSettingsFile',
+ 'url' => 'settings/{type}/{category}/{name}',
+ 'verb' => 'GET',
+ 'requirements' => [
+ 'type' => '[a-zA-Z0-9_\-]+',
+ 'category' => '[a-zA-Z0-9_\-]+',
+ 'name' => '.+',
+ ],
+ ],
// Direct Editing: Webview
['name' => 'directView#show', 'url' => '/direct/{token}', 'verb' => 'GET'],
diff --git a/composer/composer/autoload_classmap.php b/composer/composer/autoload_classmap.php
index 1bfbe4352b..c2fa2c1eda 100644
--- a/composer/composer/autoload_classmap.php
+++ b/composer/composer/autoload_classmap.php
@@ -93,6 +93,7 @@
'OCA\\Richdocuments\\TokenManager' => $baseDir . '/../lib/TokenManager.php',
'OCA\\Richdocuments\\UploadException' => $baseDir . '/../lib/UploadException.php',
'OCA\\Richdocuments\\WOPI\\Parser' => $baseDir . '/../lib/WOPI/Parser.php',
+ 'OCA\\Richdocuments\\WOPI\\SettingsUrl' => $baseDir . '/../lib/WOPI/SettingsUrl.php',
'mikehaertl\\pdftk\\Command' => $vendorDir . '/mikehaertl/php-pdftk/src/Command.php',
'mikehaertl\\pdftk\\DataFields' => $vendorDir . '/mikehaertl/php-pdftk/src/DataFields.php',
'mikehaertl\\pdftk\\FdfFile' => $vendorDir . '/mikehaertl/php-pdftk/src/FdfFile.php',
diff --git a/composer/composer/autoload_static.php b/composer/composer/autoload_static.php
index 8ca63eda2c..b5b02a2e3a 100644
--- a/composer/composer/autoload_static.php
+++ b/composer/composer/autoload_static.php
@@ -126,6 +126,7 @@ class ComposerStaticInitRichdocuments
'OCA\\Richdocuments\\TokenManager' => __DIR__ . '/..' . '/../lib/TokenManager.php',
'OCA\\Richdocuments\\UploadException' => __DIR__ . '/..' . '/../lib/UploadException.php',
'OCA\\Richdocuments\\WOPI\\Parser' => __DIR__ . '/..' . '/../lib/WOPI/Parser.php',
+ 'OCA\\Richdocuments\\WOPI\\SettingsUrl' => __DIR__ . '/..' . '/../lib/WOPI/SettingsUrl.php',
'mikehaertl\\pdftk\\Command' => __DIR__ . '/..' . '/mikehaertl/php-pdftk/src/Command.php',
'mikehaertl\\pdftk\\DataFields' => __DIR__ . '/..' . '/mikehaertl/php-pdftk/src/DataFields.php',
'mikehaertl\\pdftk\\FdfFile' => __DIR__ . '/..' . '/mikehaertl/php-pdftk/src/FdfFile.php',
diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php
index 56406912bd..858f1eefca 100644
--- a/lib/Controller/SettingsController.php
+++ b/lib/Controller/SettingsController.php
@@ -30,6 +30,7 @@
use OCP\IURLGenerator;
use OCP\PreConditionNotMetException;
use OCP\Util;
+use OCA\Richdocuments\WOPI\SettingsUrl;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\NullOutput;
@@ -623,6 +624,35 @@ public function downloadUserFile(string $fileName) {
}
}
+ /**
+ * @param string $type
+ * @param string $category
+ * @param string $name
+ *
+ * @return DataDisplayResponse
+ *
+ * @NoAdminRequired
+ * @PublicPage
+ * @NoCSRFRequired
+ **/
+ public function getSettingsFile(string $type, string $category, string $name) {
+ try {
+ $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 57ddb4619e..4b64849833 100644
--- a/lib/Controller/WopiController.php
+++ b/lib/Controller/WopiController.php
@@ -57,6 +57,7 @@
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use OCA\Richdocuments\Service\SettingsService;
+use \OCA\Richdocuments\WOPI\SettingsUrl;
#[RestrictToWopiServer]
class WopiController extends Controller {
@@ -374,30 +375,24 @@ public function getFile(string $fileId, string $access_token): JSONResponse|Stre
#[PublicPage]
#[FrontpageRoute(verb: 'GET', url: 'wopi/settings')]
public function getSettings(string $type, string $access_token): JSONResponse {
- if ($type !== 'UserSettingsUri') {
+ 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_FORBIDDEN);
}
- // user admin check
$user = $this->userManager->get($wopi->getEditorUid());
if (!$user || !$this->groupManager->isAdmin($user->getUID())) {
return new JSONResponse(['error' => 'Access denied'], Http::STATUS_FORBIDDEN);
}
- $systemFiles = $this->settingsService->getSystemFiles();
- $formattedList = $this->settingsService->getSystemFileList($systemFiles);
-
- $response = new JSONResponse($formattedList);
-
- return $response;
- } catch (UnknownTokenException|ExpiredTokenException $e) {
+ $userConfig = $this->settingsService->generateSettingsConfig($type);
+ 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) {
@@ -422,16 +417,17 @@ public function handleSettingsFile(string $fileId, string $access_token): JSONRe
if (!$content) {
throw new \Exception("Failed to read input stream.");
}
-
+
$fileContent = stream_get_contents($content);
fclose($content);
-
- // TODO: JSON based upload
- $result = $this->settingsService->uploadSystemFile($fileId, $fileContent);
-
+
+ // 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);
+
return new JSONResponse([
'status' => 'success',
- 'filename' => $newFileName,
+ 'filename' => $settingsUrl->getFileName(),
'details' => $result,
], Http::STATUS_OK);
diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php
index 35a7fd78dc..5c237e56df 100644
--- a/lib/Service/SettingsService.php
+++ b/lib/Service/SettingsService.php
@@ -10,6 +10,7 @@
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
+use OCA\Richdocuments\WOPI\SettingsUrl;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IURLGenerator;
@@ -36,6 +37,170 @@ public function __construct(
$this->cache = $cacheFactory->createDistributed(Application::APPNAME);
}
+ // TODO: Implement file caching...
+
+ /**
+ * Ensure the settings directory exists, if it doesn't exist then create it.
+ *
+ * @param SettingsUrl $settingsUrl
+ * @return ISimpleFolder
+ */
+
+ public function ensureDirectory(SettingsUrl $settingsUrl): ISimpleFolder {
+ $type = $settingsUrl->getType();
+ $category = $settingsUrl->getCategory();
+
+ try {
+ $baseFolder = $this->appData->getFolder($type);
+ } catch (NotFoundException $e) {
+ $baseFolder = $this->appData->newFolder($type);
+ }
+
+ 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 resource $fileData
+ * @return array ['stamp' => string, 'uri' => string]
+ */
+
+ public function uploadFile(SettingsUrl $settingsUrl, $fileData): array {
+ $categoryFolder = $this->ensureDirectory($settingsUrl);
+ $fileName = $settingsUrl->getFileName();
+ $newFile = $categoryFolder->newFile($fileName, $fileData);
+ $fileUri = $this->generateFileUri($settingsUrl->getType(), $settingsUrl->getCategory(), $fileName);
+
+ 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): array {
+ try {
+ $categoryFolder = $this->appData->getFolder($type . '/' . $category);
+ } catch (NotFoundException $e) {
+ return [];
+ }
+
+ $files = $categoryFolder->getDirectoryListing();
+
+ return array_map(function(ISimpleFile $file) use ($type, $category) {
+ return [
+ 'stamp' => $file->getETag(),
+ 'uri' => $this->generateFileUri($type, $category, $file->getName()),
+ ];
+ }, $files);
+ }
+
+ /**
+ * generate setting config
+ *
+ * @param string $type
+ * @return array
+ */
+ public function generateSettingsConfig(string $type): array {
+ $kind = $type === 'userconfig' ? 'user' : 'shared';
+
+ $config = [
+ 'kind' => $kind,
+ ];
+
+ $categories = $this->getAllCategories($type);
+
+ foreach ($categories as $category) {
+ $files = $this->getCategoryFileList($type, $category);
+ $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->appData->getFolder($type)->getFullDirectoryListing();
+ foreach ($directories as $dir) {
+ if ($dir instanceof ISimpleFolder) {
+ $categories[] = $dir->getName();
+ }
+ }
+ return $categories;
+ } 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 {
+ return $this->urlGenerator->linkToRouteAbsolute(
+ 'richdocuments.settings.getSettingsFile',
+ [
+ 'type' => $type,
+ '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}'.");
+ }
+ }
+
/**
* Get or create the system-wide folder in app data.
*
diff --git a/lib/WOPI/SettingsUrl.php b/lib/WOPI/SettingsUrl.php
new file mode 100644
index 0000000000..c130b5a488
--- /dev/null
+++ b/lib/WOPI/SettingsUrl.php
@@ -0,0 +1,99 @@
+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
+ * @throws InvalidArgumentException
+ */
+ 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/tests/psalm-baseline.xml b/tests/psalm-baseline.xml
index 81a432d72c..b91cb8f1ba 100644
--- a/tests/psalm-baseline.xml
+++ b/tests/psalm-baseline.xml
@@ -125,4 +125,9 @@
0]]>
+
+
+ 0]]>
+
+
From c2e7a195338be28887d86ba57c730ec317421ebf Mon Sep 17 00:00:00 2001
From: codewithvk
Date: Sun, 12 Jan 2025 00:28:47 +0530
Subject: [PATCH 12/12] Code cleanup: Remove POC helper functions
Signed-off-by: codewithvk
---
appinfo/routes.php | 8 -
lib/Controller/SettingsController.php | 188 ----------------
lib/Controller/WopiController.php | 2 +-
lib/Service/SettingsService.php | 295 +-------------------------
src/components/AdminSettings.vue | 179 ----------------
5 files changed, 3 insertions(+), 669 deletions(-)
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 3965c11013..40258c198a 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -31,19 +31,11 @@
['name' => 'settings#checkSettings', 'url' => 'settings/check', 'verb' => 'GET'],
['name' => 'settings#demoServers', 'url' => 'settings/demo', 'verb' => 'GET'],
['name' => 'settings#getFontNames', 'url' => 'settings/fonts', 'verb' => 'GET'],
- // We want to create new routes like this to store files...
['name' => 'settings#getJsonFontList', 'url' => 'settings/fonts.json', 'verb' => 'GET'],
['name' => 'settings#getFontFile', 'url' => 'settings/fonts/{name}', 'verb' => 'GET'],
['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#uploadSystemFile', 'url' => 'settings/system-files', 'verb' => 'POST'],
- ['name' => 'settings#getSystemFileList', 'url' => 'settings/system-files.json', 'verb' => 'GET' ],
- ['name' => 'settings#getSystemFile', 'url' => 'settings/system-files/{fileName}', 'verb' => 'GET'],
- ['name' => 'settings#uploadUserFile', 'url' => 'settings/user-files', 'verb' => 'POST'],
- ['name' => 'settings#getUserFileList', 'url' => 'settings/user-files.json', 'verb' => 'GET'],
- ['name' => 'settings#downloadUserFile', 'url' => 'settings/user-files/{fileName}', 'verb' => 'GET'],
- ['name' => 'settings#deleteSystemFile', 'url' => 'settings/system-files/{fileName}', 'verb' => 'DELETE'],
[
'name' => 'settings#getSettingsFile',
'url' => 'settings/{type}/{category}/{name}',
diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php
index 858f1eefca..6d6f22f8a4 100644
--- a/lib/Controller/SettingsController.php
+++ b/lib/Controller/SettingsController.php
@@ -436,194 +436,6 @@ public function uploadFontFile(): JSONResponse {
}
}
- /**
- * @return JSONResponse
- * @throws UploadException
- * @throws NotPermittedException
- * @throws Exception
- */
- public function uploadSystemFile(): JSONResponse {
- try {
- $file = $this->getUploadedFile('systemfile');
- if (!isset($file['tmp_name'], $file['name'])) {
- return new JSONResponse(['error' => 'No uploaded file'], 400);
- }
-
- $newFileResource = fopen($file['tmp_name'], 'rb');
- if ($newFileResource === false) {
- throw new UploadException('Could not open file resource');
- }
-
- $result = $this->settingsService->uploadSystemFile($file['name'], $newFileResource);
- return new JSONResponse($result);
- } catch (NotPermittedException $e) {
- $this->logger->error('Not permitted', ['exception' => $e]);
- return new JSONResponse(['error' => 'Not permitted'], 403);
- } catch (UploadException $e) {
- $this->logger->error('UploadException', ['exception' => $e]);
- return new JSONResponse(['error' => $e->getMessage()], 400);
- } catch (\Exception $e) {
- $this->logger->error('General error', ['exception' => $e]);
- return new JSONResponse(['error' => $e->getMessage()], 500);
- }
- }
-
- /**
- * @return JSONResponse
- */
- public function getSystemFileList(): JSONResponse {
- try {
- $fileNames = $this->settingsService->getSystemFileNames();
- return new JSONResponse($fileNames);
- } catch (NotPermittedException $e) {
- return new JSONResponse(['error' => 'Not permitted'], Http::STATUS_FORBIDDEN);
- }
- }
-
- /**
- * @param string $fileName
- * @return DataResponse
- *
- * @NoAdminRequired
- * @NoCSRFRequired
- * @PublicPage
- *
- */
- public function getSystemFile(string $fileName) {
- try {
- $systemFile = $this->settingsService->getSystemFile($fileName);
- $mimeType = $systemFile->getMimeType() ?: 'application/octet-stream';
- $fileContents = $systemFile->getContent();
-
- return new DataDisplayResponse(
- $fileContents,
- Http::STATUS_OK,
- [
- 'Content-Type' => $mimeType,
- 'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
- ]
- );
- } 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);
- }
- }
-
- /**
- * Delete a file by name from the "system-settings" directory.
- *
- * @NoAdminRequired
- * @PublicPage
- * @NoCSRFRequired
- *
- * @param string $fileName Name of the file to delete (URL-encoded)
- * @return JSONResponse A simple JSON indicating success or error
- */
- public function deleteSystemFile(string $fileName): JSONResponse {
- try {
- $this->settingsService->deleteSystemFile($fileName);
- return new JSONResponse(['status' => 'success'], Http::STATUS_OK);
- } 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' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
- }
-
-
- /**
- * @return JSONResponse
- */
- public function uploadUserFile(): JSONResponse {
- // Make sure we know who is uploading
- if ($this->userId === null) {
- return new JSONResponse(['error' => 'User not logged in'], 401);
- }
-
- try {
- // The key "userfile" must match the FormData append() key in Vue
- $file = $this->getUploadedFile('userfile');
- if (!isset($file['tmp_name'], $file['name'])) {
- return new JSONResponse(['error' => 'No uploaded file'], 400);
- }
-
- $newFileResource = fopen($file['tmp_name'], 'rb');
- if ($newFileResource === false) {
- throw new UploadException('Could not open file resource');
- }
-
- $result = $this->settingsService->uploadUserFile($this->userId, $file['name'], $newFileResource);
- return new JSONResponse($result); // e.g. { "size": 1234 }
- } catch (NotPermittedException $e) {
- $this->logger->error('Not permitted', ['exception' => $e]);
- return new JSONResponse(['error' => 'Not permitted'], 403);
- } catch (UploadException $e) {
- $this->logger->error('UploadException', ['exception' => $e]);
- return new JSONResponse(['error' => $e->getMessage()], 400);
- } catch (\Exception $e) {
- $this->logger->error('General error', ['exception' => $e]);
- return new JSONResponse(['error' => $e->getMessage()], 500);
- }
- }
-
- /**
- * @return JSONResponse
- */
- public function getUserFileList(): JSONResponse {
- if ($this->userId === null) {
- return new JSONResponse(['error' => 'User not logged in'], Http::STATUS_UNAUTHORIZED);
- }
-
- try {
- $fileNames = $this->settingsService->getUserFileNames($this->userId);
- return new JSONResponse($fileNames);
- } catch (NotPermittedException $e) {
- return new JSONResponse(['error' => 'Not permitted'], Http::STATUS_FORBIDDEN);
- } catch (\Exception $e) {
- return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
- }
-
- /**
- * @param string $fileName
- *
- * @NoAdminRequired
- * @NoCSRFRequired
- */
- public function downloadUserFile(string $fileName) {
- if ($this->userId === null) {
- return new JSONResponse(['error' => 'User not logged in'], Http::STATUS_UNAUTHORIZED);
- }
-
- try {
- $userFile = $this->settingsService->getUserFile($this->userId, $fileName);
- $mimeType = $userFile->getMimeType() ?: 'application/octet-stream';
- $content = $userFile->getContent(); // get file bytes
-
- // Return as DataDisplayResponse
- $response = new DataDisplayResponse(
- $content,
- Http::STATUS_OK,
- [
- 'Content-Type' => $mimeType,
- 'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
- ]
- );
-
- return $response;
- } 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) {
- return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
- }
-
/**
* @param string $type
* @param string $category
diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php
index 4b64849833..b6ff713adb 100644
--- a/lib/Controller/WopiController.php
+++ b/lib/Controller/WopiController.php
@@ -405,7 +405,7 @@ public function getSettings(string $type, string $access_token): JSONResponse {
#[NoCSRFRequired]
#[PublicPage]
#[FrontpageRoute(verb: 'POST', url: 'wopi/settings/upload')]
- public function handleSettingsFile(string $fileId, string $access_token): JSONResponse {
+ public function uploadSettingsFile(string $fileId, string $access_token): JSONResponse {
try {
$wopi = $this->wopiMapper->getWopiForToken($access_token);
diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php
index 5c237e56df..c6f2babcbe 100644
--- a/lib/Service/SettingsService.php
+++ b/lib/Service/SettingsService.php
@@ -201,299 +201,8 @@ public function getSettingsFile(string $type, string $category, string $name): I
}
}
- /**
- * Get or create the system-wide folder in app data.
- *
- * @return ISimpleFolder
- * @throws \OCP\Files\NotPermittedException
- */
- private function getSystemDataDir(): ISimpleFolder {
- try {
- return $this->appData->getFolder('system-settings');
- } catch (NotFoundException) {
- // Folder not found, create it
- return $this->appData->newFolder('system-settings');
- }
- }
-
- /**
- * Get or create the user-specific folder in app data.
- *
- * Typically you'd pass the user ID, e.g. $this->userSession->getUser()->getUID().
- *
- * @param string $userId
- * @return ISimpleFolder
- * @throws \OCP\Files\NotPermittedException
- */
- private function getUserDataDir(string $userId): ISimpleFolder {
- try {
- return $this->appData->getFolder('user-settings-' . $userId);
- } catch (NotFoundException) {
- return $this->appData->newFolder('user-settings-' . $userId);
- }
- }
-
- /**
- * Return a list of file objects from the system-wide directory.
- *
- * @return ISimpleFile[]
- * @throws \OCP\Files\NotPermittedException
- */
- public function getSystemFiles(): array {
- $dir = $this->getSystemDataDir();
- return $dir->getDirectoryListing();
- }
-
- /**
- * Get the formatted list of system-settings files.
- *
- * @param array $systemFiles Array of ISimpleFile objects
- * @return array
- */
- public function getSystemFileList(array $systemFiles): array {
- $urlGenerator = $this->urlGenerator;
- $list = array_map(
- function (ISimpleFile $f) use ($urlGenerator) {
- return [
- 'uri' => $urlGenerator->linkToRouteAbsolute(
- Application::APPNAME . '.settings.getSystemFile',
- ['fileName' => $f->getName()]
- ),
- 'stamp' => $f->getETag(),
- ];
- },
- $systemFiles
- );
-
- // Combine all ETags into a single ETag for the entire list
- $combinedEtag = md5(implode(',', array_map(fn(ISimpleFile $f) => $f->getETag(), $systemFiles)));
-
- return [
- 'files' => $list,
- 'etag' => $combinedEtag,
- ];
- }
-
- /**
- * Return a list of file objects from the user-specific directory.
- *
- * @param string $userId
- * @return ISimpleFile[]
- * @throws \OCP\Files\NotPermittedException
- */
- public function getUserFiles(string $userId): array {
- $dir = $this->getUserDataDir($userId);
- return $dir->getDirectoryListing();
- }
-
- /**
- * Return a cached list of file names for the system-wide directory.
- *
- * @return string[]
- * @throws \OCP\Files\NotPermittedException
- */
- public function getSystemFileNames(): array {
- $cacheKey = 'systemFileNames';
- $cachedNames = $this->cache->get($cacheKey);
-
- if ($cachedNames === null) {
- $files = $this->getSystemFiles();
- $cachedNames = array_map(
- static fn (ISimpleFile $f) => $f->getName(),
- $files
- );
- $this->cache->set($cacheKey, $cachedNames, self::INVALIDATE_FILE_LIST_CACHE_AFTER_SECONDS);
- }
-
- return $cachedNames;
- }
-
- /**
- * Return a cached list of file names for a specific user directory.
- *
- * @param string $userId
- * @return string[]
- * @throws \OCP\Files\NotPermittedException
- */
- public function getUserFileNames(string $userId): array {
- $cacheKey = 'userFileNames_' . $userId;
- $cachedNames = $this->cache->get($cacheKey);
-
- if ($cachedNames === null) {
- $files = $this->getUserFiles($userId);
- $cachedNames = array_map(
- static fn (ISimpleFile $f) => $f->getName(),
- $files
- );
- $this->cache->set($cacheKey, $cachedNames, self::INVALIDATE_FILE_LIST_CACHE_AFTER_SECONDS);
- }
-
- return $cachedNames;
- }
-
- /**
- * Upload or overwrite a file in the system-wide directory.
- *
- * @param string $fileName
- * @param resource|string $fileData If you have a resource handle or raw string data.
- * @return array e.g. ['size' => 1234]
- * @throws \OCP\Files\NotPermittedException
- */
- public function uploadSystemFile(string $fileName, $fileData): array {
- $dir = $this->getSystemDataDir();
- $newFile = $dir->newFile($fileName, $fileData);
-
- // Remove cache so it is rebuilt next time
- $this->cache->remove('systemFileNames');
-
- return [
- 'size' => $newFile->getSize(),
- ];
- }
-
- /**
- * Upload or overwrite a file in the user-specific directory.
- *
- * @param string $userId
- * @param string $fileName
- * @param resource|string $fileData
- * @return array
- * @throws \OCP\Files\NotPermittedException
- */
- public function uploadUserFile(string $userId, string $fileName, $fileData): array {
- $dir = $this->getUserDataDir($userId);
- $newFile = $dir->newFile($fileName, $fileData);
+ // TODO: add route for delete setting config file?
- // Invalidate cache
- $this->cache->remove('userFileNames_' . $userId);
-
- return [
- 'size' => $newFile->getSize(),
- ];
- }
-
- /**
- * Get a single file (system-wide).
- *
- * @param string $fileName
- * @return ISimpleFile
- * @throws NotFoundException
- * @throws \OCP\Files\NotPermittedException
- */
- public function getSystemFile(string $fileName): ISimpleFile {
- $dir = $this->getSystemDataDir();
- return $dir->getFile($fileName);
- }
+ // TODO: Handle installDefaultSystemFiles setting
- /**
- * Get a single file (user-specific).
- *
- * @param string $userId
- * @param string $fileName
- * @return ISimpleFile
- * @throws NotFoundException
- * @throws \OCP\Files\NotPermittedException
- */
- public function getUserFile(string $userId, string $fileName): ISimpleFile {
- $dir = $this->getUserDataDir($userId);
- return $dir->getFile($fileName);
- }
-
- /**
- * TODO: need to modify - Return the contents of a system-wide file, for example if it's JSON.
- *
- * @param string $fileName
- * @return string
- * @throws NotFoundException
- * @throws \OCP\Files\NotPermittedException
- */
- public function getSystemFileContents(string $fileName): string {
- $file = $this->getSystemFile($fileName);
- return $file->getContent();
- }
-
- /**
- * TODO: need to modify
- * Return the contents of a user-specific file.
- *
- * @param string $userId
- * @param string $fileName
- * @return string
- * @throws NotFoundException
- * @throws \OCP\Files\NotPermittedException
- */
- public function getUserFileContents(string $userId, string $fileName): string {
- $file = $this->getUserFile($userId, $fileName);
- return $file->getContent();
- }
-
- /**
- * Delete a file in the system-wide directory.
- *
- * @param string $fileName
- * @return void
- * @throws \OCP\Files\NotPermittedException
- */
- public function deleteSystemFile(string $fileName): void {
- $dir = $this->getSystemDataDir();
- if ($dir->fileExists($fileName)) {
- $dir->getFile($fileName)->delete();
- }
- $this->cache->remove('systemFileNames');
- }
-
- /**
- * Delete a file in the user-specific directory.
- *
- * @param string $userId
- * @param string $fileName
- * @return void
- * @throws \OCP\Files\NotPermittedException
- */
- public function deleteUserFile(string $userId, string $fileName): void {
- $dir = $this->getUserDataDir($userId);
- if ($dir->fileExists($fileName)) {
- $dir->getFile($fileName)->delete();
- }
- $this->cache->remove('userFileNames_' . $userId);
- }
-
- /**
- * Example of a method to bulk-install default files, similar to "installDefaultFonts" in FontService.
- * This reads from a local assets folder, but adapt as needed.
- *
- * @throws Exception
- */
- public function installDefaultSystemFiles(): void {
- $dirPath = __DIR__ . '/../../assets/system-data'; // example location
-
- if (!is_dir($dirPath)) {
- throw new Exception("Directory \"$dirPath\" does not exist!");
- }
-
- $handle = opendir($dirPath);
- if (!$handle) {
- throw new Exception("Failed opening directory \"$dirPath\"!");
- }
-
- while (false !== ($fileName = readdir($handle))) {
- // skip dot-files or any irrelevant files
- if (str_starts_with($fileName, '.') || str_ends_with($fileName, '.txt')) {
- continue;
- }
-
- $filePath = $dirPath . '/' . $fileName;
- if (!is_file($filePath)) {
- continue;
- }
-
- // Read file from disk (example)
- $fileHandle = fopen($filePath, 'r');
- if (!$fileHandle) {
- continue;
- }
-
- $this->uploadSystemFile($fileName, $fileHandle);
- }
- }
}
diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue
index a82463122f..f9a8635854 100644
--- a/src/components/AdminSettings.vue
+++ b/src/components/AdminSettings.vue
@@ -24,68 +24,6 @@
:access-token-t-t-l="accessTokenTTL" />
-
-
{{ t('richdocuments', 'Upload System File (Minimal Example)') }}
-
{{ t('richdocuments', 'Pick a file to upload into the system-settings folder:') }}
-
-
-
-
-
-
System Files
-
-
-
-
-
-
-
Upload File to User Settings (Minimal Example)
-
Select a file to store in "user-settings-{{ userId }}" folder:
-
-
-
-
-
-
User Files
-
Load and see your user-specific files stored in user-settings-{{ userId }}.
-
-
-
-
-
-
{{ t('richdocuments', 'Could not establish connection to the Collabora Online server.') }}
@@ -533,12 +471,6 @@ export default {
productName: loadState('richdocuments', 'productName', 'Nextcloud Office'),
hasNextcloudBranding: loadState('richdocuments', 'hasNextcloudBranding', true),
- uploadingSystemFile: false,
- systemFiles: [],
- uploadingUserFile: false,
- userFiles: [],
- loadingUserFiles: false,
-
serverMode: '',
serverError: SERVER_STATE_LOADING,
errorMessage: null,
@@ -639,13 +571,6 @@ export default {
} else {
console.error('User not authenticated')
}
- if (this.isSetup) {
- this.getSystemFiles()
- }
-
- if (this.isSetup) {
- this.getUserFiles()
- }
},
beforeMount() {
for (const key in this.initial.settings) {
@@ -772,110 +697,6 @@ export default {
console.error(error)
})
},
- uploadSystemFile(event) {
- const file = event.target.files[0]
- if (!file) {
- return
- }
- this.uploadingSystemFile = true
-
- const formData = new FormData()
- formData.append('systemfile', file)
-
- const url = this.$options.methods.generateSystemFilesUrl()
-
- axios.post(url, formData, {
- headers: { 'Content-Type': 'multipart/form-data' },
- }).then((response) => {
- }).catch((error) => {
- console.error('System file upload error:', error)
- showError(error?.response?.data?.error ?? 'Unknown error')
- }).finally(() => {
- this.uploadingSystemFile = false
- event.target.value = ''
- })
- },
-
- generateSystemFilesUrl() {
- return generateUrl('/apps/richdocuments/settings/system-files')
- },
-
- getSystemFiles() {
- const url = generateUrl('/apps/richdocuments/settings/system-files.json')
- axios.get(url)
- .then((response) => {
- this.systemFiles = response.data
- })
- .catch((error) => {
- console.error('Failed to load system files:', error)
- })
- },
-
- deleteSystemFile(fileName) {
- if (!window.confirm(`Are you sure you want to delete "${fileName}"?`)) {
- return
- }
-
- const url = generateUrl('/apps/richdocuments/settings/system-files/' + encodeURIComponent(fileName))
-
- axios.delete(url)
- .then((response) => {
- this.getSystemFiles()
- })
- .catch((error) => {
- console.error('Delete error', error.response?.data || error)
- showError(t('richdocuments', 'Could not delete file') + ': ' + fileName)
- })
- },
-
- downloadUrl(fileName) {
- return generateUrl('/apps/richdocuments/settings/system-files/' + encodeURIComponent(fileName))
- },
-
- uploadUserFile(event) {
- const file = event.target.files[0]
- if (!file) {
- return
- }
- this.uploadingUserFile = true
-
- const formData = new FormData()
- formData.append('userfile', file)
-
- const url = generateUrl('/apps/richdocuments/settings/user-files')
- axios.post(url, formData, {
- headers: { 'Content-Type': 'multipart/form-data' },
- })
- .then((response) => {
- })
- .catch((error) => {
- console.error('User file upload error:', error)
- })
- .finally(() => {
- this.uploadingUserFile = false
- event.target.value = ''
- })
- },
-
- getUserFiles() {
- this.loadingUserFiles = true
- const url = generateUrl('/apps/richdocuments/settings/user-files.json')
- axios.get(url)
- .then((response) => {
- this.userFiles = response.data
- })
- .catch((error) => {
- console.error('Failed to load user files:', error)
- showError('Could not load your user files')
- })
- .finally(() => {
- this.loadingUserFiles = false
- })
- },
-
- downloadUserFileUrl(fileName) {
- return generateUrl('/apps/richdocuments/settings/user-files/' + encodeURIComponent(fileName))
- },
async updateUseGroups(enabled) {
if (typeof enabled === 'boolean') {