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]]>
+
+