From ba4c76eafe0ed291007bfa6117d12d242ff5c69b Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 25 May 2026 23:23:43 +0200 Subject: [PATCH] feat(preferences): generic per-user preferences endpoint Adds a generic per-user key/value preferences endpoint backed by Nextcloud IConfig user values. This backs the auto-mount of the shared @conduction/nextcloud-vue CnSupportDialog, which persists a small per-user 'seen' flag cross-device without a bespoke endpoint per feature. Keys are sanitised to a safe charset and namespaced under 'pref_' so callers cannot reach arbitrary IConfig user values. --- appinfo/routes.php | 4 + lib/Controller/PreferencesController.php | 152 +++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 lib/Controller/PreferencesController.php diff --git a/appinfo/routes.php b/appinfo/routes.php index dace6846..51ee08b1 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -11,6 +11,10 @@ ['name' => 'settings#create', 'url' => '/api/settings', 'verb' => 'POST'], ['name' => 'settings#load', 'url' => '/api/settings/load', 'verb' => 'POST'], + // Generic per-user preferences (used by shared nextcloud-vue widgets, e.g. CnSupportDialog). + ['name' => 'preferences#getPreference', 'url' => '/api/preferences/{key}', 'verb' => 'GET'], + ['name' => 'preferences#setPreference', 'url' => '/api/preferences/{key}', 'verb' => 'PUT'], + // Prometheus metrics endpoint. ['name' => 'metrics#index', 'url' => '/api/metrics', 'verb' => 'GET'], // Health check endpoint. diff --git a/lib/Controller/PreferencesController.php b/lib/Controller/PreferencesController.php new file mode 100644 index 00000000..8a51e2cd --- /dev/null +++ b/lib/Controller/PreferencesController.php @@ -0,0 +1,152 @@ + + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://github.com/ConductionNL/openbuilt + */ + +declare(strict_types=1); + +namespace OCA\OpenBuilt\Controller; + +use OCA\OpenBuilt\AppInfo\Application; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IUserSession; + +/** + * Per-user preferences controller. + */ +class PreferencesController extends Controller +{ + /** + * Constructor. + * + * @param IRequest $request The request. + * @param IConfig $config The Nextcloud config (user values). + * @param IUserSession $userSession The user session. + */ + public function __construct( + IRequest $request, + private readonly IConfig $config, + private readonly IUserSession $userSession, + ) { + parent::__construct(appName: Application::APP_ID, request: $request); + + }//end __construct() + + /** + * Read a per-user preference value. + * + * @param string $key The preference key (kebab/alphanumeric). + * + * @return JSONResponse `{value: string|null}`. + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function getPreference(string $key): JSONResponse + { + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse(data: ['message' => 'Not logged in'], statusCode: Http::STATUS_UNAUTHORIZED); + } + + $safeKey = $this->sanitizeKey(key: $key); + if ($safeKey === '') { + return new JSONResponse(data: ['message' => 'Invalid key'], statusCode: Http::STATUS_BAD_REQUEST); + } + + $value = $this->config->getUserValue( + userId: $user->getUID(), + appName: Application::APP_ID, + key: 'pref_'.$safeKey, + default: '' + ); + + $stored = null; + if ($value !== '') { + $stored = $value; + } + + return new JSONResponse(data: ['value' => $stored]); + + }//end getPreference() + + /** + * Write a per-user preference value. An empty value clears it. + * + * @param string $key The preference key (kebab/alphanumeric). + * @param string $value The value to store (empty string clears it). + * + * @return JSONResponse `{value: string|null}`. + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function setPreference(string $key, string $value=''): JSONResponse + { + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse(data: ['message' => 'Not logged in'], statusCode: Http::STATUS_UNAUTHORIZED); + } + + $safeKey = $this->sanitizeKey(key: $key); + if ($safeKey === '') { + return new JSONResponse(data: ['message' => 'Invalid key'], statusCode: Http::STATUS_BAD_REQUEST); + } + + $stored = null; + if ($value === '') { + $this->config->deleteUserValue( + userId: $user->getUID(), + appName: Application::APP_ID, + key: 'pref_'.$safeKey + ); + } else { + $this->config->setUserValue( + userId: $user->getUID(), + appName: Application::APP_ID, + key: 'pref_'.$safeKey, + value: $value + ); + $stored = $value; + } + + return new JSONResponse(data: ['value' => $stored]); + + }//end setPreference() + + /** + * Restrict keys to a safe charset so callers cannot reach arbitrary + * IConfig user values outside the `pref_` namespace. + * + * @param string $key The raw key. + * + * @return string The sanitised key, or '' when nothing safe remains. + */ + private function sanitizeKey(string $key): string + { + $safe = preg_replace(pattern: '/[^a-z0-9-]/', replacement: '', subject: strtolower($key)); + return substr((string) $safe, offset: 0, length: 64); + + }//end sanitizeKey() +}//end class