From ee5d0896b0d290b4a34c3c8f7c87ad03509bf606 Mon Sep 17 00:00:00 2001 From: Thomas Piccirello Date: Sun, 10 Aug 2025 21:36:23 -0700 Subject: [PATCH] WebAPI: Support persisting WebUI client preferences This provides a mechanism for persisting WebUI client preferences that are distinct from the broader qBittorrent preferences. These preferences apply exclusively to the WebUI. --- src/webui/api/appcontroller.cpp | 122 ++++++++++++++++++++++++++++++++ src/webui/api/appcontroller.h | 11 ++- src/webui/webapplication.h | 1 + 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index e2e465acd..0aa059636 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -51,17 +51,20 @@ #include "base/bittorrent/session.h" #include "base/global.h" #include "base/interfaces/iapplication.h" +#include "base/logger.h" #include "base/net/downloadmanager.h" #include "base/net/portforwarder.h" #include "base/net/proxyconfigurationmanager.h" #include "base/path.h" #include "base/preferences.h" +#include "base/profile.h" #include "base/rss/rss_autodownloader.h" #include "base/rss/rss_session.h" #include "base/torrentfileguard.h" #include "base/torrentfileswatcher.h" #include "base/utils/datetime.h" #include "base/utils/fs.h" +#include "base/utils/io.h" #include "base/utils/misc.h" #include "base/utils/net.h" #include "base/utils/password.h" @@ -84,6 +87,42 @@ const QString KEY_FILE_METADATA_CREATION_DATE = u"creation_date"_s; const QString KEY_FILE_METADATA_LAST_ACCESS_DATE = u"last_access_date"_s; const QString KEY_FILE_METADATA_LAST_MODIFICATION_DATE = u"last_modification_date"_s; +const int CLIENT_DATA_FILE_MAX_SIZE = 1024 * 1024; +const QString CLIENT_DATA_FILE_NAME = u"web_data.json"_s; + +AppController::AppController(IApplication *app, QObject *parent) + : APIController(app, parent) +{ + m_clientDataFilePath = specialFolderLocation(SpecialFolder::Data) / Path(CLIENT_DATA_FILE_NAME); + if (m_clientDataFilePath.exists()) + { + const auto readResult = Utils::IO::readFile(m_clientDataFilePath, CLIENT_DATA_FILE_MAX_SIZE); + if (!readResult) + { + LogMsg(tr("Failed to load web client data. %1").arg(readResult.error().message), Log::WARNING); + return; + } + + QJsonParseError jsonError; + const QJsonDocument jsonDoc = QJsonDocument::fromJson(readResult.value(), &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + LogMsg(tr("Failed to parse web client data. File: \"%1\". Error: \"%2\"") + .arg(m_clientDataFilePath.toString(), jsonError.errorString()), Log::WARNING); + return; + } + + if (!jsonDoc.isObject()) + { + LogMsg(tr("Failed to load web client data. File: \"%1\". Error: \"Invalid data format\"") + .arg(m_clientDataFilePath.toString()), Log::WARNING); + return; + } + + m_clientData = jsonDoc.object(); + } +} + void AppController::webapiVersionAction() { setResult(API_VERSION.toString()); @@ -1312,6 +1351,89 @@ void AppController::setCookiesAction() setResult(QString()); } +void AppController::clientDataAction() +{ + const QString keysParam {params()[u"keys"_s]}; + if (keysParam.isEmpty()) + { + setResult(m_clientData); + return; + } + + QJsonParseError jsonError; + const auto keysJsonDocument = QJsonDocument::fromJson(keysParam.toUtf8(), &jsonError); + if (jsonError.error != QJsonParseError::NoError) + throw APIError(APIErrorType::BadParams, jsonError.errorString()); + if (!keysJsonDocument.isArray()) + throw APIError(APIErrorType::BadParams, tr("keys must be an array")); + + QJsonObject clientData; + for (const QJsonValue &keysJsonVal : asConst(keysJsonDocument.array())) + { + if (!keysJsonVal.isString()) + throw APIError(APIErrorType::BadParams, tr("key must be a string")); + + const QString &key = keysJsonVal.toString(); + if (const auto iter = m_clientData.constFind(key); iter != m_clientData.constEnd()) + clientData.insert(key, iter.value()); + } + + setResult(clientData); +} + +void AppController::setClientDataAction() +{ + requireParams({u"data"_s}); + QJsonParseError jsonError; + const auto dataJsonDocument = QJsonDocument::fromJson(params()[u"data"_s].toUtf8(), &jsonError); + if (jsonError.error != QJsonParseError::NoError) + throw APIError(APIErrorType::BadParams, jsonError.errorString()); + if (!dataJsonDocument.isObject()) + throw APIError(APIErrorType::BadParams, tr("data must be an object")); + + QJsonObject clientData = m_clientData; + bool dataModified = false; + const QJsonObject dataJsonObject = dataJsonDocument.object(); + for (auto it = dataJsonObject.constBegin(), end = dataJsonObject.constEnd(); it != end; ++it) + { + const QString &key = it.key(); + const QJsonValue &value = it.value(); + + if (value.isNull()) + { + if (auto it = clientData.find(key); it != clientData.end()) + { + clientData.erase(it); + dataModified = true; + } + } + else + { + const auto &existingValue = clientData.constFind(key); + if ((existingValue == clientData.constEnd()) || (existingValue.value() != value)) + { + clientData.insert(key, value); + dataModified = true; + } + } + } + + if (!dataModified) + return; + + const QByteArray json = QJsonDocument(clientData).toJson(QJsonDocument::Compact); + if (json.size() > CLIENT_DATA_FILE_MAX_SIZE) + throw APIError(APIErrorType::BadParams, tr("data must not be larger than %1 bytes").arg(CLIENT_DATA_FILE_MAX_SIZE)); + const nonstd::expected result = Utils::IO::saveToFile(m_clientDataFilePath, json); + if (!result) + { + throw APIError(APIErrorType::Conflict, tr("Failed to save web client data. Error: \"%1\"") + .arg(result.error())); + } + + m_clientData = clientData; +} + void AppController::networkInterfaceListAction() { QJsonArray ifaceList; diff --git a/src/webui/api/appcontroller.h b/src/webui/api/appcontroller.h index dea33eada..ffe71d2f4 100644 --- a/src/webui/api/appcontroller.h +++ b/src/webui/api/appcontroller.h @@ -30,7 +30,10 @@ #pragma once +#include + #include "apicontroller.h" +#include "base/path.h" class AppController : public APIController { @@ -38,7 +41,7 @@ class AppController : public APIController Q_DISABLE_COPY_MOVE(AppController) public: - using APIController::APIController; + explicit AppController(IApplication *app, QObject *parent = nullptr); private slots: void webapiVersionAction(); @@ -52,7 +55,13 @@ private slots: void getDirectoryContentAction(); void cookiesAction(); void setCookiesAction(); + void clientDataAction(); + void setClientDataAction(); void networkInterfaceListAction(); void networkInterfaceAddressListAction(); + +private: + Path m_clientDataFilePath; + QJsonObject m_clientData; }; diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 889da879b..f5b2858fb 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -148,6 +148,7 @@ private: { // <, HTTP method> {{u"app"_s, u"sendTestEmail"_s}, Http::METHOD_POST}, + {{u"app"_s, u"setClientData"_s}, Http::METHOD_POST}, {{u"app"_s, u"setCookies"_s}, Http::METHOD_POST}, {{u"app"_s, u"setPreferences"_s}, Http::METHOD_POST}, {{u"app"_s, u"shutdown"_s}, Http::METHOD_POST},