diff --git a/WebAPI_Changelog.md b/WebAPI_Changelog.md index 3c85fadd4202..7be0c987a648 100644 --- a/WebAPI_Changelog.md +++ b/WebAPI_Changelog.md @@ -4,6 +4,8 @@ * [#23163](https://github.com/qbittorrent/qBittorrent/pull/23163) * `torrents/add` endpoint now supports downloading from a search plugin via the `downloader` parameter * `torrents/fetchMetadata` endpoint now supports fetching from a search plugin via the `downloader` parameter +* [#23088](https://github.com/qbittorrent/qBittorrent/pull/23088) + * Add `clientdata/load` and `clientdata/store` endpoints for managing WebUI-specific client settings and other shared data ## 2.13.0 * [#23045](https://github.com/qbittorrent/qBittorrent/pull/23045) diff --git a/src/webui/CMakeLists.txt b/src/webui/CMakeLists.txt index 377a8356ba2e..4597ed4681ec 100644 --- a/src/webui/CMakeLists.txt +++ b/src/webui/CMakeLists.txt @@ -5,6 +5,7 @@ add_library(qbt_webui STATIC api/apistatus.h api/appcontroller.h api/authcontroller.h + api/clientdatacontroller.h api/isessionmanager.h api/logcontroller.h api/rsscontroller.h @@ -14,6 +15,7 @@ add_library(qbt_webui STATIC api/torrentscontroller.h api/transfercontroller.h api/serialize/serialize_torrent.h + clientdatastorage.h webapplication.h webui.h @@ -22,6 +24,7 @@ add_library(qbt_webui STATIC api/apierror.cpp api/appcontroller.cpp api/authcontroller.cpp + api/clientdatacontroller.cpp api/logcontroller.cpp api/rsscontroller.cpp api/searchcontroller.cpp @@ -30,6 +33,7 @@ add_library(qbt_webui STATIC api/torrentscontroller.cpp api/transfercontroller.cpp api/serialize/serialize_torrent.cpp + clientdatastorage.cpp webapplication.cpp webui.cpp ) diff --git a/src/webui/api/clientdatacontroller.cpp b/src/webui/api/clientdatacontroller.cpp new file mode 100644 index 000000000000..5667522f5b0f --- /dev/null +++ b/src/webui/api/clientdatacontroller.cpp @@ -0,0 +1,88 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2025 Thomas Piccirello + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "clientdatacontroller.h" + +#include +#include +#include + +#include "base/global.h" +#include "base/interfaces/iapplication.h" +#include "base/logger.h" +#include "apierror.h" +#include "webui/clientdatastorage.h" + +ClientDataController::ClientDataController(ClientDataStorage *clientDataStorage, IApplication *app, QObject *parent) + : APIController(app, parent) + , m_clientDataStorage {clientDataStorage} +{ +} + +void ClientDataController::loadAction() +{ + const QString keysParam {params()[u"keys"_s]}; + if (keysParam.isEmpty()) + { + setResult(m_clientDataStorage->loadData()); + 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")); + + QStringList keys; + for (const QJsonValue &keysJsonVal : asConst(keysJsonDocument.array())) + { + if (!keysJsonVal.isString()) + throw APIError(APIErrorType::BadParams, tr("Items of `keys` must be strings")); + + keys << keysJsonVal.toString(); + } + + setResult(m_clientDataStorage->loadData(keys)); +} + +void ClientDataController::storeAction() +{ + 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")); + + const nonstd::expected result = m_clientDataStorage->storeData(dataJsonDocument.object()); + if (!result) + throw APIError(APIErrorType::Conflict, result.error()); +} diff --git a/src/webui/api/clientdatacontroller.h b/src/webui/api/clientdatacontroller.h new file mode 100644 index 000000000000..fd48450d8858 --- /dev/null +++ b/src/webui/api/clientdatacontroller.h @@ -0,0 +1,49 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2025 Thomas Piccirello + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include "apicontroller.h" + +class ClientDataStorage; + +class ClientDataController final : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(ClientDataController) + +public: + ClientDataController(ClientDataStorage *clientDataStorage, IApplication *app, QObject *parent = nullptr); + +private slots: + void loadAction(); + void storeAction(); + +private: + ClientDataStorage *m_clientDataStorage = nullptr; +}; diff --git a/src/webui/clientdatastorage.cpp b/src/webui/clientdatastorage.cpp new file mode 100644 index 000000000000..b633e16b0cf2 --- /dev/null +++ b/src/webui/clientdatastorage.cpp @@ -0,0 +1,138 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2025 Thomas Piccirello + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "clientdatastorage.h" + +#include +#include + +#include "base/global.h" +#include "base/logger.h" +#include "base/path.h" +#include "base/profile.h" +#include "base/utils/io.h" + +const int CLIENT_DATA_FILE_MAX_SIZE = 1024 * 1024; // 1 MiB +const QString CLIENT_DATA_FILE_NAME = u"web_clientdata.json"_s; + +ClientDataStorage::ClientDataStorage(QObject *parent) + : QObject(parent) + , m_clientDataFilePath(specialFolderLocation(SpecialFolder::Data) / Path(CLIENT_DATA_FILE_NAME)) +{ + if (!m_clientDataFilePath.exists()) + return; + + 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(); +} + +nonstd::expected ClientDataStorage::storeData(const QJsonObject &object) +{ + QJsonObject clientData = m_clientData; + bool dataModified = false; + for (auto it = object.constBegin(), end = object.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.find(key); + if (existingValue == clientData.end()) + { + clientData.insert(key, value); + dataModified = true; + } + else if (existingValue.value() != value) + { + existingValue.value() = value; + dataModified = true; + } + } + } + + if (dataModified) + { + const QByteArray json = QJsonDocument(clientData).toJson(QJsonDocument::Compact); + if (json.size() > CLIENT_DATA_FILE_MAX_SIZE) + return nonstd::make_unexpected(tr("Total web client 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) + return nonstd::make_unexpected(tr("Failed to save web client data. Error: \"%1\"").arg(result.error())); + + m_clientData = clientData; + } + + return {}; +} + +QJsonObject ClientDataStorage::loadData() const +{ + return m_clientData; +} + +QJsonObject ClientDataStorage::loadData(const QStringList &keys) const +{ + QJsonObject clientData; + for (const QString &key : keys) + { + if (const auto iter = m_clientData.constFind(key); iter != m_clientData.constEnd()) + clientData.insert(key, iter.value()); + } + return clientData; +} diff --git a/src/webui/clientdatastorage.h b/src/webui/clientdatastorage.h new file mode 100644 index 000000000000..5a4641b4cede --- /dev/null +++ b/src/webui/clientdatastorage.h @@ -0,0 +1,51 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2025 Thomas Piccirello + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include + +#include "base/3rdparty/expected.hpp" +#include "base/path.h" + +class ClientDataStorage final : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(ClientDataStorage) + +public: + ClientDataStorage(QObject *parent = nullptr); + + nonstd::expected storeData(const QJsonObject &object); + QJsonObject loadData() const; + QJsonObject loadData(const QStringList &keys) const; + +private: + Path m_clientDataFilePath; + QJsonObject m_clientData; +}; diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index a28e002b53e3..b771a863cddd 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -60,6 +60,7 @@ #include "api/apierror.h" #include "api/appcontroller.h" #include "api/authcontroller.h" +#include "api/clientdatacontroller.h" #include "api/logcontroller.h" #include "api/rsscontroller.h" #include "api/searchcontroller.h" @@ -67,6 +68,7 @@ #include "api/torrentcreatorcontroller.h" #include "api/torrentscontroller.h" #include "api/transfercontroller.h" +#include "clientdatastorage.h" const int MAX_ALLOWED_FILESIZE = 10 * 1024 * 1024; const QString DEFAULT_SESSION_COOKIE_NAME = u"SID"_s; @@ -158,6 +160,7 @@ WebApplication::WebApplication(IApplication *app, QObject *parent) , m_cacheID {QString::number(Utils::Random::rand(), 36)} , m_authController {new AuthController(this, app, this)} , m_torrentCreationManager {new BitTorrent::TorrentCreationManager(app, this)} + , m_clientDataStorage {new ClientDataStorage(this)} { declarePublicAPI(u"auth/login"_s); @@ -749,6 +752,7 @@ void WebApplication::sessionStart() m_sessions[m_currentSession->id()] = m_currentSession; m_currentSession->registerAPIController(u"app"_s, new AppController(app(), m_currentSession)); + m_currentSession->registerAPIController(u"clientdata"_s, new ClientDataController(m_clientDataStorage, app(), m_currentSession)); m_currentSession->registerAPIController(u"log"_s, new LogController(app(), m_currentSession)); m_currentSession->registerAPIController(u"torrentcreator"_s, new TorrentCreatorController(m_torrentCreationManager, app(), m_currentSession)); m_currentSession->registerAPIController(u"rss"_s, new RSSController(app(), m_currentSession)); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index f30d5a06f981..1e6a890760bf 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -57,6 +57,7 @@ inline const Utils::Version<3, 2> API_VERSION {2, 13, 1}; class APIController; class AuthController; +class ClientDataStorage; class WebApplication; namespace BitTorrent @@ -153,6 +154,7 @@ class WebApplication final : public ApplicationComponent {{u"app"_s, u"shutdown"_s}, Http::METHOD_POST}, {{u"auth"_s, u"login"_s}, Http::METHOD_POST}, {{u"auth"_s, u"logout"_s}, Http::METHOD_POST}, + {{u"clientdata"_s, u"store"_s}, Http::METHOD_POST}, {{u"rss"_s, u"addFeed"_s}, Http::METHOD_POST}, {{u"rss"_s, u"addFolder"_s}, Http::METHOD_POST}, {{u"rss"_s, u"markAsRead"_s}, Http::METHOD_POST}, @@ -259,4 +261,5 @@ class WebApplication final : public ApplicationComponent QList m_prebuiltHeaders; BitTorrent::TorrentCreationManager *m_torrentCreationManager = nullptr; + ClientDataStorage *m_clientDataStorage = nullptr; };