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.
This commit is contained in:
Thomas Piccirello 2025-08-10 21:36:23 -07:00
commit ee5d0896b0
No known key found for this signature in database
3 changed files with 133 additions and 1 deletions

View file

@ -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<void, QString> 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;

View file

@ -30,7 +30,10 @@
#pragma once
#include <QJsonObject>
#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;
};

View file

@ -148,6 +148,7 @@ private:
{
// <<controller name, action name>, 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},