From 7b73d3fb5c7c433ca58a12e0a89d5003a6f5e7de Mon Sep 17 00:00:00 2001 From: thalieht Date: Fri, 13 Oct 2023 21:30:25 +0300 Subject: [PATCH 01/24] Add I2P settings to WebUI PR #19700. --- src/webui/api/appcontroller.cpp | 28 +++++++ src/webui/webapplication.h | 2 +- src/webui/www/private/views/preferences.html | 88 ++++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index 346e9a8d9..de314fd5c 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -193,6 +193,16 @@ void AppController::preferencesAction() data[u"max_uploads"_s] = session->maxUploads(); data[u"max_uploads_per_torrent"_s] = session->maxUploadsPerTorrent(); + // I2P + data[u"i2p_enabled"_s] = session->isI2PEnabled(); + data[u"i2p_address"_s] = session->I2PAddress(); + data[u"i2p_port"_s] = session->I2PPort(); + data[u"i2p_mixed_mode"_s] = session->I2PMixedMode(); + data[u"i2p_inbound_quantity"_s] = session->I2PInboundQuantity(); + data[u"i2p_outbound_quantity"_s] = session->I2POutboundQuantity(); + data[u"i2p_inbound_length"_s] = session->I2PInboundLength(); + data[u"i2p_outbound_length"_s] = session->I2POutboundLength(); + // Proxy Server const auto *proxyManager = Net::ProxyConfigurationManager::instance(); Net::ProxyConfiguration proxyConf = proxyManager->proxyConfiguration(); @@ -628,6 +638,24 @@ void AppController::setPreferencesAction() if (hasKey(u"max_uploads_per_torrent"_s)) session->setMaxUploadsPerTorrent(it.value().toInt()); + // I2P + if (hasKey(u"i2p_enabled"_s)) + session->setI2PEnabled(it.value().toBool()); + if (hasKey(u"i2p_address"_s)) + session->setI2PAddress(it.value().toString()); + if (hasKey(u"i2p_port"_s)) + session->setI2PPort(it.value().toInt()); + if (hasKey(u"i2p_mixed_mode"_s)) + session->setI2PMixedMode(it.value().toBool()); + if (hasKey(u"i2p_inbound_quantity"_s)) + session->setI2PInboundQuantity(it.value().toInt()); + if (hasKey(u"i2p_outbound_quantity"_s)) + session->setI2POutboundQuantity(it.value().toInt()); + if (hasKey(u"i2p_inbound_length"_s)) + session->setI2PInboundLength(it.value().toInt()); + if (hasKey(u"i2p_outbound_length"_s)) + session->setI2POutboundLength(it.value().toInt()); + // Proxy Server auto *proxyManager = Net::ProxyConfigurationManager::instance(); Net::ProxyConfiguration proxyConf = proxyManager->proxyConfiguration(); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 364d5ee41..929f6e5e6 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -52,7 +52,7 @@ #include "base/utils/version.h" #include "api/isessionmanager.h" -inline const Utils::Version<3, 2> API_VERSION {2, 9, 2}; +inline const Utils::Version<3, 2> API_VERSION {2, 9, 3}; class APIController; class AuthController; diff --git a/src/webui/www/private/views/preferences.html b/src/webui/www/private/views/preferences.html index e53a0bd8c..867149951 100644 --- a/src/webui/www/private/views/preferences.html +++ b/src/webui/www/private/views/preferences.html @@ -349,6 +349,33 @@ +
+ + + + + + + + + + + +
+ + + + + + + +
+
+ + +
+
+
QBT_TR(Proxy Server)QBT_TR[CONTEXT=OptionsDialog] @@ -1420,6 +1447,38 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + +
@@ -1455,6 +1514,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD updateMaxConnecPerTorrentEnabled: updateMaxConnecPerTorrentEnabled, updateMaxUploadsEnabled: updateMaxUploadsEnabled, updateMaxUploadsPerTorrentEnabled: updateMaxUploadsPerTorrentEnabled, + updateI2PSettingsEnabled: updateI2PSettingsEnabled, updatePeerProxySettings: updatePeerProxySettings, updatePeerProxyAuthSettings: updatePeerProxyAuthSettings, updateFilterSettings: updateFilterSettings, @@ -1644,6 +1704,13 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD $('max_uploads_per_torrent_value').setProperty('disabled', !isMaxUploadsPerTorrentEnabled); }; + const updateI2PSettingsEnabled = function() { + const isI2PEnabled = $('i2pEnabledCheckbox').getProperty('checked'); + $('i2pAddress').setProperty('disabled', !isI2PEnabled); + $('i2pPort').setProperty('disabled', !isI2PEnabled); + $('i2pMixedMode').setProperty('disabled', !isI2PEnabled); + }; + const updatePeerProxySettings = function() { const proxyType = $('peer_proxy_type_select').getProperty('value'); const isProxyDisabled = (proxyType === "None"); @@ -2023,6 +2090,13 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD } updateMaxUploadsPerTorrentEnabled(); + // I2P + $('i2pEnabledCheckbox').setProperty('checked', pref.i2p_enabled); + $('i2pAddress').setProperty('value', pref.i2p_address); + $('i2pPort').setProperty('value', pref.i2p_port); + $('i2pMixedMode').setProperty('checked', pref.i2p_mixed_mode); + updateI2PSettingsEnabled(); + // Proxy Server $('peer_proxy_type_select').setProperty('value', pref.proxy_type); $('peer_proxy_host_text').setProperty('value', pref.proxy_ip); @@ -2241,6 +2315,10 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD $('peerTurnoverCutoff').setProperty('value', pref.peer_turnover_cutoff); $('peerTurnoverInterval').setProperty('value', pref.peer_turnover_interval); $('requestQueueSize').setProperty('value', pref.request_queue_size); + $('i2pInboundQuantity').setProperty('value', pref.i2p_inbound_quantity); + $('i2pOutboundQuantity').setProperty('value', pref.i2p_outbound_quantity); + $('i2pInboundLength').setProperty('value', pref.i2p_inbound_length); + $('i2pOutboundLength').setProperty('value', pref.i2p_outbound_length); } } }).send(); @@ -2360,6 +2438,12 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD } settings.set('max_uploads_per_torrent', max_uploads_per_torrent); + // I2P + settings.set('i2p_enabled', $('i2pEnabledCheckbox').getProperty('checked')); + settings.set('i2p_address', $('i2pAddress').getProperty('value')); + settings.set('i2p_port', $('i2pPort').getProperty('value').toInt()); + settings.set('i2p_mixed_mode', $('i2pMixedMode').getProperty('checked')); + // Proxy Server settings.set('proxy_type', $('peer_proxy_type_select').getProperty('value')); settings.set('proxy_ip', $('peer_proxy_host_text').getProperty('value')); @@ -2673,6 +2757,10 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD settings.set('peer_turnover_cutoff', $('peerTurnoverCutoff').getProperty('value')); settings.set('peer_turnover_interval', $('peerTurnoverInterval').getProperty('value')); settings.set('request_queue_size', $('requestQueueSize').getProperty('value')); + settings.set('i2p_inbound_quantity', $('i2pInboundQuantity').getProperty('value')); + settings.set('i2p_outbound_quantity', $('i2pOutboundQuantity').getProperty('value')); + settings.set('i2p_inbound_length', $('i2pInboundLength').getProperty('value')); + settings.set('i2p_outbound_length', $('i2pOutboundLength').getProperty('value')); // Send it to qBT const json_str = JSON.encode(settings); From dc31e82d0058e7835acb13d0a57595150fdea08c Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Tue, 17 Oct 2023 21:31:55 +0300 Subject: [PATCH 02/24] CI: Use dynamic MSVC run-time library Otherwise, there is a conflict of run-time libraries used (since Qt DLLs are still use dynamic MSVCRT) that leads to strange errors. PR #19718. Closes #19701. --- .github/workflows/ci_windows.yaml | 4 ++-- src/app/cmdoptions.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci_windows.yaml b/.github/workflows/ci_windows.yaml index f7e310f60..37900f7f5 100644 --- a/.github/workflows/ci_windows.yaml +++ b/.github/workflows/ci_windows.yaml @@ -101,7 +101,7 @@ jobs: -DBOOST_ROOT="${{ env.boost_path }}" ` -DBUILD_SHARED_LIBS=OFF ` -Ddeprecated-functions=OFF ` - -Dstatic_runtime=ON ` + -Dstatic_runtime=OFF ` -DVCPKG_TARGET_TRIPLET=x64-windows-static-release cmake --build build cmake --install build @@ -117,7 +117,7 @@ jobs: -DCMAKE_TOOLCHAIN_FILE="${{ env.RUNVCPKG_VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" ` -DBOOST_ROOT="${{ env.boost_path }}" ` -DLibtorrentRasterbar_DIR="${{ env.libtorrent_path }}/lib/cmake/LibtorrentRasterbar" ` - -DMSVC_RUNTIME_DYNAMIC=OFF ` + -DMSVC_RUNTIME_DYNAMIC=ON ` -DQT6=ON ` -DTESTING=ON ` -DVCPKG_TARGET_TRIPLET=x64-windows-static-release ` diff --git a/src/app/cmdoptions.cpp b/src/app/cmdoptions.cpp index 0b993c3a6..9b3dddbfa 100644 --- a/src/app/cmdoptions.cpp +++ b/src/app/cmdoptions.cpp @@ -373,7 +373,7 @@ QBtCommandLineParameters parseCommandLine(const QStringList &args) if ((arg.startsWith(u"--") && !arg.endsWith(u".torrent")) || (arg.startsWith(u'-') && (arg.size() == 2))) - { + { // Parse known parameters if (arg == SHOW_HELP_OPTION) { From 4b56bdd36ff88c59f769dee28b27888723a4fe7f Mon Sep 17 00:00:00 2001 From: Chocobo1 Date: Thu, 19 Oct 2023 12:07:36 +0800 Subject: [PATCH 03/24] Combine CSS properties PR #19730. --- src/webui/www/private/css/style.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webui/www/private/css/style.css b/src/webui/www/private/css/style.css index 14a598749..7f7f201bd 100644 --- a/src/webui/www/private/css/style.css +++ b/src/webui/www/private/css/style.css @@ -178,8 +178,7 @@ a.propButton img { } .scrollableMenu { - overflow-x: hidden; - overflow-y: auto; + overflow: hidden auto; } /* context menu specific */ From 42d7465cbabb4d0be58b166bd374d1d1d8976162 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Sat, 28 Oct 2023 10:41:53 +0300 Subject: [PATCH 04/24] Prevent crash due to race condition when adding magnet link PR #19792. Closes #19780. Closes #19790. --- src/base/bittorrent/sessionimpl.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index aeaa54698..e1011777c 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -2435,6 +2435,11 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id) return false; const lt::torrent_handle nativeHandle = downloadedMetadataIter.value(); + m_downloadedMetadata.erase(downloadedMetadataIter); + + if (!nativeHandle.is_valid()) + return true; + #ifdef QBT_USES_LIBTORRENT2 const InfoHash infoHash {nativeHandle.info_hashes()}; if (infoHash.isHybrid()) @@ -2445,7 +2450,7 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id) m_downloadedMetadata.remove((altID == downloadedMetadataIter.key()) ? id : altID); } #endif - m_downloadedMetadata.erase(downloadedMetadataIter); + m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_files); return true; } From a1317e24f9c16c54b59219a50c7522253d59500f Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Sat, 28 Oct 2023 10:42:37 +0300 Subject: [PATCH 05/24] Fix Enter key behavior when add new torrent PR #19787. Closes #19760. --- src/gui/properties/propertieswidget.cpp | 1 + src/gui/torrentcontentwidget.cpp | 30 +++++++++++++++++++++---- src/gui/torrentcontentwidget.h | 7 ++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/gui/properties/propertieswidget.cpp b/src/gui/properties/propertieswidget.cpp index 572da74d9..6b846e2bb 100644 --- a/src/gui/properties/propertieswidget.cpp +++ b/src/gui/properties/propertieswidget.cpp @@ -82,6 +82,7 @@ PropertiesWidget::PropertiesWidget(QWidget *parent) m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine); m_ui->filesList->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Open); + m_ui->filesList->setOpenByEnterKey(true); // SIGNAL/SLOTS connect(m_ui->selectAllButton, &QPushButton::clicked, m_ui->filesList, &TorrentContentWidget::checkAll); diff --git a/src/gui/torrentcontentwidget.cpp b/src/gui/torrentcontentwidget.cpp index 594fe76f1..680dfd172 100644 --- a/src/gui/torrentcontentwidget.cpp +++ b/src/gui/torrentcontentwidget.cpp @@ -89,10 +89,6 @@ TorrentContentWidget::TorrentContentWidget(QWidget *parent) const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut); connect(renameFileHotkey, &QShortcut::activated, this, &TorrentContentWidget::renameSelectedFile); - const auto *openFileHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut); - connect(openFileHotkeyReturn, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile); - const auto *openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut); - connect(openFileHotkeyEnter, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile); connect(model(), &QAbstractItemModel::modelReset, this, &TorrentContentWidget::expandRecursively); } @@ -118,6 +114,32 @@ void TorrentContentWidget::refresh() setUpdatesEnabled(true); } +bool TorrentContentWidget::openByEnterKey() const +{ + return m_openFileHotkeyEnter; +} + +void TorrentContentWidget::setOpenByEnterKey(const bool value) +{ + if (value == openByEnterKey()) + return; + + if (value) + { + m_openFileHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut); + connect(m_openFileHotkeyReturn, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile); + m_openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut); + connect(m_openFileHotkeyEnter, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile); + } + else + { + delete m_openFileHotkeyEnter; + m_openFileHotkeyEnter = nullptr; + delete m_openFileHotkeyReturn; + m_openFileHotkeyReturn = nullptr; + } +} + TorrentContentWidget::DoubleClickAction TorrentContentWidget::doubleClickAction() const { return m_doubleClickAction; diff --git a/src/gui/torrentcontentwidget.h b/src/gui/torrentcontentwidget.h index 35620ee42..4baccb883 100644 --- a/src/gui/torrentcontentwidget.h +++ b/src/gui/torrentcontentwidget.h @@ -34,6 +34,8 @@ #include "base/bittorrent/downloadpriority.h" #include "base/pathfwd.h" +class QShortcut; + namespace BitTorrent { class Torrent; @@ -78,6 +80,9 @@ public: BitTorrent::TorrentContentHandler *contentHandler() const; void refresh(); + bool openByEnterKey() const; + void setOpenByEnterKey(bool value); + DoubleClickAction doubleClickAction() const; void setDoubleClickAction(DoubleClickAction action); @@ -118,4 +123,6 @@ private: TorrentContentFilterModel *m_filterModel; DoubleClickAction m_doubleClickAction = DoubleClickAction::Rename; ColumnsVisibilityMode m_columnsVisibilityMode = ColumnsVisibilityMode::Editable; + QShortcut *m_openFileHotkeyEnter = nullptr; + QShortcut *m_openFileHotkeyReturn = nullptr; }; From a396e0df26e25e1c09fe5b1e836ca599f086097e Mon Sep 17 00:00:00 2001 From: AgentConDier Date: Sun, 29 Oct 2023 10:39:30 +0100 Subject: [PATCH 06/24] WebUI: Fix duplicate scrollbar on Transfer List The overlay scrollbars introduced in Firefox 100 take up no space, breaking the existing overflow detection. Add an extra check for `scrollHeight != clientHeight` which is able to detect an overflow independent of scrollbar style. PR #19779. --------- Co-authored-by: Chocobo1 --- src/webui/www/private/scripts/dynamicTable.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/webui/www/private/scripts/dynamicTable.js b/src/webui/www/private/scripts/dynamicTable.js index c997446d6..7cd5591ea 100644 --- a/src/webui/www/private/scripts/dynamicTable.js +++ b/src/webui/www/private/scripts/dynamicTable.js @@ -111,7 +111,8 @@ window.qBittorrent.DynamicTable = (function() { let n = 2; - while (panel.clientWidth != panel.offsetWidth && n > 0) { // is panel vertical scrollbar visible ? + // is panel vertical scrollbar visible or does panel content not fit? + while (((panel.clientWidth != panel.offsetWidth) || (panel.clientHeight != panel.scrollHeight)) && (n > 0)) { --n; h -= 0.5; $(this.dynamicTableDivId).style.height = h + 'px'; From bb6d69f8b7f2786f14c29418b279b2bd5b78b545 Mon Sep 17 00:00:00 2001 From: Chocobo1 Date: Sun, 29 Oct 2023 17:41:38 +0800 Subject: [PATCH 07/24] =?UTF-8?q?Remove=20"Physical=20memory=20(RAM)=20usa?= =?UTF-8?q?ge=20limit"=20option=20on=20macOS=C2=A0#19805?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This has no effect on macOS. https://stackoverflow.com/questions/3274385/how-to-limit-memory-of-a-os-x-program-ulimit-v-neither-m-are-working Closes #18656. PR #19805. --- src/app/application.cpp | 6 +++--- src/app/application.h | 2 +- src/gui/advancedsettings.cpp | 6 +++--- src/gui/advancedsettings.h | 7 ++++++- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/app/application.cpp b/src/app/application.cpp index 0fb60f83f..f70293031 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -375,7 +375,7 @@ void Application::setMemoryWorkingSetLimit(const int size) return; m_storeMemoryWorkingSetLimit = size; -#ifdef QBT_USES_LIBTORRENT2 +#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) applyMemoryWorkingSetLimit(); #endif } @@ -773,7 +773,7 @@ int Application::exec() printf("%s\n", qUtf8Printable(loadingStr)); #endif -#ifdef QBT_USES_LIBTORRENT2 +#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) applyMemoryWorkingSetLimit(); #endif @@ -1080,7 +1080,7 @@ void Application::shutdownCleanup(QSessionManager &manager) } #endif -#ifdef QBT_USES_LIBTORRENT2 +#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) void Application::applyMemoryWorkingSetLimit() const { const size_t MiB = 1024 * 1024; diff --git a/src/app/application.h b/src/app/application.h index ee69f5afc..76a12ffb3 100644 --- a/src/app/application.h +++ b/src/app/application.h @@ -154,7 +154,7 @@ private: void runExternalProgram(const QString &programTemplate, const BitTorrent::Torrent *torrent) const; void sendNotificationEmail(const BitTorrent::Torrent *torrent); -#ifdef QBT_USES_LIBTORRENT2 +#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) void applyMemoryWorkingSetLimit() const; #endif diff --git a/src/gui/advancedsettings.cpp b/src/gui/advancedsettings.cpp index af665fdae..b116f2d4d 100644 --- a/src/gui/advancedsettings.cpp +++ b/src/gui/advancedsettings.cpp @@ -63,7 +63,7 @@ namespace // qBittorrent section QBITTORRENT_HEADER, RESUME_DATA_STORAGE, -#ifdef QBT_USES_LIBTORRENT2 +#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) MEMORY_WORKING_SET_LIMIT, #endif #if defined(Q_OS_WIN) @@ -195,7 +195,7 @@ void AdvancedSettings::saveAdvancedSettings() const BitTorrent::Session *const session = BitTorrent::Session::instance(); session->setResumeDataStorageType(m_comboBoxResumeDataStorage.currentData().value()); -#ifdef QBT_USES_LIBTORRENT2 +#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) // Physical memory (RAM) usage limit app()->setMemoryWorkingSetLimit(m_spinBoxMemoryWorkingSetLimit.value()); #endif @@ -451,7 +451,7 @@ void AdvancedSettings::loadAdvancedSettings() m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType()))); addRow(RESUME_DATA_STORAGE, tr("Resume data storage type (requires restart)"), &m_comboBoxResumeDataStorage); -#ifdef QBT_USES_LIBTORRENT2 +#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) // Physical memory (RAM) usage limit m_spinBoxMemoryWorkingSetLimit.setMinimum(1); m_spinBoxMemoryWorkingSetLimit.setMaximum(std::numeric_limits::max()); diff --git a/src/gui/advancedsettings.h b/src/gui/advancedsettings.h index 652cd88ac..3622fa787 100644 --- a/src/gui/advancedsettings.h +++ b/src/gui/advancedsettings.h @@ -30,6 +30,7 @@ #include +#include #include #include #include @@ -88,7 +89,11 @@ private: QCheckBox m_checkBoxCoalesceRW; #else QComboBox m_comboBoxDiskIOType; - QSpinBox m_spinBoxMemoryWorkingSetLimit, m_spinBoxHashingThreads; + QSpinBox m_spinBoxHashingThreads; +#endif + +#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) + QSpinBox m_spinBoxMemoryWorkingSetLimit; #endif #if defined(QBT_USES_LIBTORRENT2) && TORRENT_USE_I2P From 31e0ab2c7091362acf51e6c299c6f63c9696675f Mon Sep 17 00:00:00 2001 From: Chocobo1 Date: Fri, 27 Oct 2023 16:06:01 +0800 Subject: [PATCH 08/24] GHA CI: don't override cmake default CXXFLAGS --- .github/workflows/ci_windows.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_windows.yaml b/.github/workflows/ci_windows.yaml index 37900f7f5..4f8a1ee00 100644 --- a/.github/workflows/ci_windows.yaml +++ b/.github/workflows/ci_windows.yaml @@ -90,11 +90,12 @@ jobs: --recurse-submodules ` https://github.com/arvidn/libtorrent.git cd libtorrent + $env:CXXFLAGS+=" /guard:cf" + $env:LDFLAGS+=" /guard:cf" cmake ` -B build ` -G "Ninja" ` -DCMAKE_BUILD_TYPE=RelWithDebInfo ` - -DCMAKE_CXX_FLAGS=/guard:cf ` -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ` -DCMAKE_INSTALL_PREFIX="${{ env.libtorrent_path }}" ` -DCMAKE_TOOLCHAIN_FILE="${{ env.RUNVCPKG_VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" ` @@ -108,11 +109,11 @@ jobs: - name: Build qBittorrent run: | + $env:CXXFLAGS+=" /WX" cmake ` -B build ` -G "Ninja" ` -DCMAKE_BUILD_TYPE=RelWithDebInfo ` - -DCMAKE_CXX_FLAGS="/WX" ` -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ` -DCMAKE_TOOLCHAIN_FILE="${{ env.RUNVCPKG_VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" ` -DBOOST_ROOT="${{ env.boost_path }}" ` From 3c0747fb875833a6f381a19f13148845c45faae3 Mon Sep 17 00:00:00 2001 From: iomezk Date: Tue, 31 Oct 2023 07:18:35 +0000 Subject: [PATCH 09/24] Add missing main window icon PR #19815. --- Co-authored-by: Vladimir Golovnev --- src/icons/icons.qrc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/icons/icons.qrc b/src/icons/icons.qrc index 62744009c..7d23cc8ca 100644 --- a/src/icons/icons.qrc +++ b/src/icons/icons.qrc @@ -327,6 +327,7 @@ qbittorrent-tray-dark.svg qbittorrent-tray-light.svg qbittorrent-tray.svg + qbittorrent-tray.svg queued.svg ratio.svg reannounce.svg From d5c0c094f20e3158af0b2b9f760cbf84adf52d3e Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Tue, 31 Oct 2023 10:20:23 +0300 Subject: [PATCH 10/24] Don't forget to delete TorrentContentAdaptor instance PR #19825. Closes #19816. --- src/gui/addnewtorrentdialog.cpp | 6 +++--- src/gui/addnewtorrentdialog.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index b77b930b4..7b9cbeb3e 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -767,7 +767,7 @@ void AddNewTorrentDialog::contentLayoutChanged() const auto contentLayout = static_cast(m_ui->contentLayoutComboBox->currentIndex()); m_contentAdaptor->applyContentLayout(contentLayout); - m_ui->contentTreeView->setContentHandler(m_contentAdaptor); // to cause reloading + m_ui->contentTreeView->setContentHandler(m_contentAdaptor.get()); // to cause reloading } void AddNewTorrentDialog::saveTorrentFile() @@ -992,7 +992,7 @@ void AddNewTorrentDialog::setupTreeview() if (m_torrentParams.filePaths.isEmpty()) m_torrentParams.filePaths = m_torrentInfo.filePaths(); - m_contentAdaptor = new TorrentContentAdaptor(m_torrentInfo, m_torrentParams.filePaths, m_torrentParams.filePriorities); + m_contentAdaptor = std::make_unique(m_torrentInfo, m_torrentParams.filePaths, m_torrentParams.filePriorities); const auto contentLayout = static_cast(m_ui->contentLayoutComboBox->currentIndex()); m_contentAdaptor->applyContentLayout(contentLayout); @@ -1013,7 +1013,7 @@ void AddNewTorrentDialog::setupTreeview() m_contentAdaptor->prioritizeFiles(priorities); } - m_ui->contentTreeView->setContentHandler(m_contentAdaptor); + m_ui->contentTreeView->setContentHandler(m_contentAdaptor.get()); m_filterLine->blockSignals(false); diff --git a/src/gui/addnewtorrentdialog.h b/src/gui/addnewtorrentdialog.h index 3e2891037..910275344 100644 --- a/src/gui/addnewtorrentdialog.h +++ b/src/gui/addnewtorrentdialog.h @@ -116,7 +116,7 @@ private: void showEvent(QShowEvent *event) override; Ui::AddNewTorrentDialog *m_ui = nullptr; - TorrentContentAdaptor *m_contentAdaptor = nullptr; + std::unique_ptr m_contentAdaptor; BitTorrent::MagnetUri m_magnetURI; BitTorrent::TorrentInfo m_torrentInfo; int m_savePathIndex = -1; From ee853d8751854219839441ae350ba261dc5c5af4 Mon Sep 17 00:00:00 2001 From: Vitaly Cheptsov <4348897+vit9696@users.noreply.github.com> Date: Wed, 1 Nov 2023 09:19:52 +0300 Subject: [PATCH 11/24] Fix .torrent file upload on iPadOS Mobile Safari on iOS does report `ios` platform, but iPadOS reports `mac` instead. It is common sense to check for touch points when this happens to differentiate Mac and iPad. PR #19822. Closes #19057. --------- Co-authored-by: Vladimir Golovnev --- src/webui/www/private/upload.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webui/www/private/upload.html b/src/webui/www/private/upload.html index 731a4a525..b2e2b018c 100644 --- a/src/webui/www/private/upload.html +++ b/src/webui/www/private/upload.html @@ -167,7 +167,7 @@ window.parent.closeWindows(); }); - if (Browser.platform === 'ios') { + if ((Browser.platform === 'ios') || ((Browser.platform === 'mac') && (navigator.maxTouchPoints > 1))) { $('fileselect').accept = ".torrent"; } From b284d40430adcd118a58c6dbdf79bdf1563f48da Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Wed, 1 Nov 2023 09:21:39 +0300 Subject: [PATCH 12/24] Update size of selected files when selection is changed PR #19827. Closes #19818. --- src/gui/addnewtorrentdialog.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index 7b9cbeb3e..147b90f04 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -30,6 +30,7 @@ #include "addnewtorrentdialog.h" #include +#include #include #include @@ -141,10 +142,11 @@ class AddNewTorrentDialog::TorrentContentAdaptor final { public: TorrentContentAdaptor(BitTorrent::TorrentInfo &torrentInfo, PathList &filePaths - , QVector &filePriorities) + , QVector &filePriorities, std::function onFilePrioritiesChanged) : m_torrentInfo {torrentInfo} , m_filePaths {filePaths} , m_filePriorities {filePriorities} + , m_onFilePrioritiesChanged {std::move(onFilePrioritiesChanged)} { Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount())); @@ -255,6 +257,8 @@ public: { Q_ASSERT(priorities.size() == filesCount()); m_filePriorities = priorities; + if (m_onFilePrioritiesChanged) + m_onFilePrioritiesChanged(); } Path actualStorageLocation() const override @@ -275,6 +279,7 @@ private: BitTorrent::TorrentInfo &m_torrentInfo; PathList &m_filePaths; QVector &m_filePriorities; + std::function m_onFilePrioritiesChanged; Path m_originalRootFolder; BitTorrent::TorrentContentLayout m_currentContentLayout; }; @@ -992,7 +997,8 @@ void AddNewTorrentDialog::setupTreeview() if (m_torrentParams.filePaths.isEmpty()) m_torrentParams.filePaths = m_torrentInfo.filePaths(); - m_contentAdaptor = std::make_unique(m_torrentInfo, m_torrentParams.filePaths, m_torrentParams.filePriorities); + m_contentAdaptor = std::make_unique(m_torrentInfo, m_torrentParams.filePaths + , m_torrentParams.filePriorities, [this] { updateDiskSpaceLabel(); }); const auto contentLayout = static_cast(m_ui->contentLayoutComboBox->currentIndex()); m_contentAdaptor->applyContentLayout(contentLayout); From a14b50e48cbc5d7b7aa186b8f79a519fc244aaa5 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Wed, 1 Nov 2023 09:22:32 +0300 Subject: [PATCH 13/24] Correctly handle changing save path of torrent w/o metadata PR #19829. Closes #19824. --- src/base/bittorrent/torrentimpl.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index f11256557..7cab602d7 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -1811,6 +1811,7 @@ void TorrentImpl::moveStorage(const Path &newPath, const MoveStorageContext cont { if (!hasMetadata()) { + m_savePath = newPath; m_session->handleTorrentSavePathChanged(this); return; } From 54dffa10512400757a7f9d47317f3a2bb26dc2fd Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Thu, 2 Nov 2023 11:12:32 +0300 Subject: [PATCH 14/24] Allow to request torrents count via WebAPI PR #19831. Closes #19731. --- src/webui/api/torrentscontroller.cpp | 5 +++++ src/webui/api/torrentscontroller.h | 1 + 2 files changed, 6 insertions(+) diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 57799f9e4..dfb6a378e 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -232,6 +232,11 @@ namespace } } +void TorrentsController::countAction() +{ + setResult(QString::number(BitTorrent::Session::instance()->torrents().count())); +} + // Returns all the torrents in JSON format. // The return value is a JSON-formatted list of dictionaries. // The dictionary keys are: diff --git a/src/webui/api/torrentscontroller.h b/src/webui/api/torrentscontroller.h index 779959ca4..e961505f8 100644 --- a/src/webui/api/torrentscontroller.h +++ b/src/webui/api/torrentscontroller.h @@ -39,6 +39,7 @@ public: using APIController::APIController; private slots: + void countAction(); void infoAction(); void propertiesAction(); void trackersAction(); From 837d39dac76c1215c9a9655edef66e456d4a1a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartu=20=C3=96zen?= Date: Thu, 2 Nov 2023 11:35:42 +0300 Subject: [PATCH 15/24] WebUI: Fix incorrect subcategory sorting PR #19833. Closes #19756. --- src/webui/www/private/scripts/client.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/webui/www/private/scripts/client.js b/src/webui/www/private/scripts/client.js index 8cfacac9d..afd2440ca 100644 --- a/src/webui/www/private/scripts/client.js +++ b/src/webui/www/private/scripts/client.js @@ -488,7 +488,21 @@ window.addEvent('load', function() { Object.each(category_list, function(category) { sortedCategories.push(category.name); }); - sortedCategories.sort(); + sortedCategories.sort(function(category1, category2) { + for (let i = 0; i < Math.min(category1.length, category2.length); ++i) { + if (category1[i] === "/" && category2[i] !== "/") { + return -1; + } + else if (category1[i] !== "/" && category2[i] === "/") { + return 1; + } + else if (category1[i] !== category2[i]) { + return category1[i].localeCompare(category2[i]); + } + } + + return category1.length - category2.length; + }); for (let i = 0; i < sortedCategories.length; ++i) { const categoryName = sortedCategories[i]; From 903173b8f1aeee3149ac5f8ba9a641e195dbc292 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Fri, 21 Jul 2023 08:37:11 +0300 Subject: [PATCH 16/24] Always use QStyledItemDelegate as base of delegate classes PR #19340. --- src/gui/previewlistdelegate.cpp | 15 ++++--------- src/gui/previewlistdelegate.h | 5 +++-- src/gui/previewselectdialog.cpp | 38 ++++++++++++++++++--------------- src/gui/previewselectdialog.h | 6 +----- 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/src/gui/previewlistdelegate.cpp b/src/gui/previewlistdelegate.cpp index 89075bcfb..d89815756 100644 --- a/src/gui/previewlistdelegate.cpp +++ b/src/gui/previewlistdelegate.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -30,13 +31,12 @@ #include #include -#include -#include "base/utils/misc.h" +#include "base/utils/string.h" #include "previewselectdialog.h" PreviewListDelegate::PreviewListDelegate(QObject *parent) - : QItemDelegate(parent) + : QStyledItemDelegate(parent) { } @@ -44,15 +44,8 @@ void PreviewListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &o { painter->save(); - QStyleOptionViewItem opt = QItemDelegate::setOptions(index, option); - drawBackground(painter, opt, index); - switch (index.column()) { - case PreviewSelectDialog::SIZE: - QItemDelegate::drawDisplay(painter, opt, option.rect, Utils::Misc::friendlyUnit(index.data().toLongLong())); - break; - case PreviewSelectDialog::PROGRESS: { const qreal progress = (index.data().toReal() * 100); @@ -65,7 +58,7 @@ void PreviewListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &o break; default: - QItemDelegate::paint(painter, option, index); + QStyledItemDelegate::paint(painter, option, index); break; } diff --git a/src/gui/previewlistdelegate.h b/src/gui/previewlistdelegate.h index ac0daab21..b9fc286f6 100644 --- a/src/gui/previewlistdelegate.h +++ b/src/gui/previewlistdelegate.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -28,11 +29,11 @@ #pragma once -#include +#include #include "progressbarpainter.h" -class PreviewListDelegate final : public QItemDelegate +class PreviewListDelegate final : public QStyledItemDelegate { Q_OBJECT Q_DISABLE_COPY_MOVE(PreviewListDelegate) diff --git a/src/gui/previewselectdialog.cpp b/src/gui/previewselectdialog.cpp index 21546e081..f79ff54f4 100644 --- a/src/gui/previewselectdialog.cpp +++ b/src/gui/previewselectdialog.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2011 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -70,16 +71,19 @@ PreviewSelectDialog::PreviewSelectDialog(QWidget *parent, const BitTorrent::Torr const Preferences *pref = Preferences::instance(); // Preview list - m_previewListModel = new QStandardItemModel(0, NB_COLUMNS, this); - m_previewListModel->setHeaderData(NAME, Qt::Horizontal, tr("Name")); - m_previewListModel->setHeaderData(SIZE, Qt::Horizontal, tr("Size")); - m_previewListModel->setHeaderData(PROGRESS, Qt::Horizontal, tr("Progress")); + auto *previewListModel = new QStandardItemModel(0, NB_COLUMNS, this); + previewListModel->setHeaderData(NAME, Qt::Horizontal, tr("Name")); + previewListModel->setHeaderData(SIZE, Qt::Horizontal, tr("Size")); + previewListModel->setHeaderData(PROGRESS, Qt::Horizontal, tr("Progress")); m_ui->previewList->setAlternatingRowColors(pref->useAlternatingRowColors()); - m_ui->previewList->setModel(m_previewListModel); + m_ui->previewList->setUniformRowHeights(true); + m_ui->previewList->setModel(previewListModel); m_ui->previewList->hideColumn(FILE_INDEX); - m_listDelegate = new PreviewListDelegate(this); - m_ui->previewList->setItemDelegate(m_listDelegate); + + auto *listDelegate = new PreviewListDelegate(this); + m_ui->previewList->setItemDelegate(listDelegate); + // Fill list in const QVector fp = torrent->filesProgress(); for (int i = 0; i < torrent->filesCount(); ++i) @@ -87,20 +91,20 @@ PreviewSelectDialog::PreviewSelectDialog(QWidget *parent, const BitTorrent::Torr const Path filePath = torrent->filePath(i); if (Utils::Misc::isPreviewable(filePath)) { - int row = m_previewListModel->rowCount(); - m_previewListModel->insertRow(row); - m_previewListModel->setData(m_previewListModel->index(row, NAME), filePath.filename()); - m_previewListModel->setData(m_previewListModel->index(row, SIZE), torrent->fileSize(i)); - m_previewListModel->setData(m_previewListModel->index(row, PROGRESS), fp[i]); - m_previewListModel->setData(m_previewListModel->index(row, FILE_INDEX), i); + int row = previewListModel->rowCount(); + previewListModel->insertRow(row); + previewListModel->setData(previewListModel->index(row, NAME), filePath.filename()); + previewListModel->setData(previewListModel->index(row, SIZE), Utils::Misc::friendlyUnit(torrent->fileSize(i))); + previewListModel->setData(previewListModel->index(row, PROGRESS), fp[i]); + previewListModel->setData(previewListModel->index(row, FILE_INDEX), i); } } - m_previewListModel->sort(NAME); + previewListModel->sort(NAME); m_ui->previewList->header()->setContextMenuPolicy(Qt::CustomContextMenu); m_ui->previewList->header()->setFirstSectionMovable(true); m_ui->previewList->header()->setSortIndicator(0, Qt::AscendingOrder); - m_ui->previewList->selectionModel()->select(m_previewListModel->index(0, NAME), QItemSelectionModel::Select | QItemSelectionModel::Rows); + m_ui->previewList->selectionModel()->select(previewListModel->index(0, NAME), QItemSelectionModel::Select | QItemSelectionModel::Rows); connect(m_ui->previewList->header(), &QWidget::customContextMenuRequested, this, &PreviewSelectDialog::displayColumnHeaderMenu); @@ -129,7 +133,7 @@ void PreviewSelectDialog::previewButtonClicked() // File if (!path.exists()) { - const bool isSingleFile = (m_previewListModel->rowCount() == 1); + const bool isSingleFile = (m_ui->previewList->model()->rowCount() == 1); QWidget *parent = isSingleFile ? this->parentWidget() : this; QMessageBox::critical(parent, tr("Preview impossible") , tr("Sorry, we can't preview this file: \"%1\".").arg(path.toString())); @@ -199,6 +203,6 @@ void PreviewSelectDialog::showEvent(QShowEvent *event) } // Only one file, no choice - if (m_previewListModel->rowCount() <= 1) + if (m_ui->previewList->model()->rowCount() <= 1) previewButtonClicked(); } diff --git a/src/gui/previewselectdialog.h b/src/gui/previewselectdialog.h index 76a42bad7..7f4a1d985 100644 --- a/src/gui/previewselectdialog.h +++ b/src/gui/previewselectdialog.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2011 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -33,8 +34,6 @@ #include "base/path.h" #include "base/settingvalue.h" -class QStandardItemModel; - namespace BitTorrent { class Torrent; @@ -44,7 +43,6 @@ namespace Ui { class PreviewSelectDialog; } -class PreviewListDelegate; class PreviewSelectDialog final : public QDialog { @@ -79,8 +77,6 @@ private: void saveWindowState(); Ui::PreviewSelectDialog *m_ui = nullptr; - QStandardItemModel *m_previewListModel = nullptr; - PreviewListDelegate *m_listDelegate = nullptr; const BitTorrent::Torrent *m_torrent = nullptr; bool m_headerStateInitialized = false; From c2dd53cee69451495958d08f7913f9204d131759 Mon Sep 17 00:00:00 2001 From: xavier2k6 <42386382+xavier2k6@users.noreply.github.com> Date: Sat, 4 Nov 2023 09:09:47 +0000 Subject: [PATCH 17/24] Use appropriate icon for "moving" torrents in transfer list PR #19821. --- src/gui/transferlistmodel.cpp | 4 +++- src/gui/transferlistmodel.h | 1 + src/webui/www/private/scripts/dynamicTable.js | 5 ++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/gui/transferlistmodel.cpp b/src/gui/transferlistmodel.cpp index 9011a9d6f..c59604b2b 100644 --- a/src/gui/transferlistmodel.cpp +++ b/src/gui/transferlistmodel.cpp @@ -117,6 +117,7 @@ TransferListModel::TransferListModel(QObject *parent) , m_completedIcon {UIThemeManager::instance()->getIcon(u"checked-completed"_s, u"completed"_s)} , m_downloadingIcon {UIThemeManager::instance()->getIcon(u"downloading"_s)} , m_errorIcon {UIThemeManager::instance()->getIcon(u"error"_s)} + , m_movingIcon {UIThemeManager::instance()->getIcon(u"set-location"_s)} , m_pausedIcon {UIThemeManager::instance()->getIcon(u"stopped"_s, u"media-playback-pause"_s)} , m_queuedIcon {UIThemeManager::instance()->getIcon(u"queued"_s)} , m_stalledDLIcon {UIThemeManager::instance()->getIcon(u"stalledDL"_s)} @@ -710,8 +711,9 @@ QIcon TransferListModel::getIconByState(const BitTorrent::TorrentState state) co case BitTorrent::TorrentState::CheckingDownloading: case BitTorrent::TorrentState::CheckingUploading: case BitTorrent::TorrentState::CheckingResumeData: - case BitTorrent::TorrentState::Moving: return m_checkingIcon; + case BitTorrent::TorrentState::Moving: + return m_movingIcon; case BitTorrent::TorrentState::Unknown: case BitTorrent::TorrentState::MissingFiles: case BitTorrent::TorrentState::Error: diff --git a/src/gui/transferlistmodel.h b/src/gui/transferlistmodel.h index f0d7e8ad1..e2da9f007 100644 --- a/src/gui/transferlistmodel.h +++ b/src/gui/transferlistmodel.h @@ -137,6 +137,7 @@ private: QIcon m_completedIcon; QIcon m_downloadingIcon; QIcon m_errorIcon; + QIcon m_movingIcon; QIcon m_pausedIcon; QIcon m_queuedIcon; QIcon m_stalledDLIcon; diff --git a/src/webui/www/private/scripts/dynamicTable.js b/src/webui/www/private/scripts/dynamicTable.js index 7cd5591ea..c511b9ae3 100644 --- a/src/webui/www/private/scripts/dynamicTable.js +++ b/src/webui/www/private/scripts/dynamicTable.js @@ -1002,10 +1002,13 @@ window.qBittorrent.DynamicTable = (function() { case "checkingUP": case "queuedForChecking": case "checkingResumeData": - case "moving": state = "force-recheck"; img_path = "images/force-recheck.svg"; break; + case "moving": + state = "moving"; + img_path = "images/set-location.svg"; + break; case "error": case "unknown": case "missingFiles": From c7dfc1ded2c60f7f5749eda579eb460ee3b3ac37 Mon Sep 17 00:00:00 2001 From: Chocobo1 Date: Fri, 4 Aug 2023 13:01:08 +0800 Subject: [PATCH 18/24] Remove column span The property widget already has scroll bars and thus we don't need this span. Closes #15000. PR #19404. --- src/gui/properties/propertieswidget.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/properties/propertieswidget.ui b/src/gui/properties/propertieswidget.ui index 01123af83..2dcae04c2 100644 --- a/src/gui/properties/propertieswidget.ui +++ b/src/gui/properties/propertieswidget.ui @@ -192,7 +192,7 @@ - + @@ -382,7 +382,7 @@ - + From 2c2252d7d908393acb03e104513665c684e5e66f Mon Sep 17 00:00:00 2001 From: vik Date: Tue, 7 Nov 2023 21:32:25 -0800 Subject: [PATCH 19/24] WebUI: Remove extra ')' in QBT_TR macro PR #19894. --- src/webui/www/private/rename_files.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webui/www/private/rename_files.html b/src/webui/www/private/rename_files.html index 633501c57..01ede024c 100644 --- a/src/webui/www/private/rename_files.html +++ b/src/webui/www/private/rename_files.html @@ -3,7 +3,7 @@ - QBT_TR(Renaming))QBT_TR[CONTEXT=TorrentContentTreeView] + QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView] From 786c09e981ba5b17affd2f02c73909b4f79c6d28 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Fri, 10 Nov 2023 07:18:42 +0300 Subject: [PATCH 20/24] Drop WebUI default credentials PR #19777. --- src/app/application.cpp | 36 +++-- src/app/application.h | 4 + src/app/cmdoptions.cpp | 8 +- src/app/cmdoptions.h | 2 +- src/base/interfaces/iapplication.h | 5 + src/base/preferences.cpp | 80 ++++++----- src/base/preferences.h | 52 +++---- src/base/utils/password.cpp | 22 ++- src/base/utils/password.h | 3 + src/gui/ipsubnetwhitelistoptionsdialog.cpp | 4 +- src/gui/mainwindow.cpp | 2 +- src/gui/optionsdialog.cpp | 150 +++++++++++---------- src/gui/optionsdialog.h | 8 +- src/gui/optionsdialog.ui | 52 ++++--- src/webui/api/appcontroller.cpp | 58 ++++---- src/webui/api/authcontroller.cpp | 26 ++-- src/webui/api/authcontroller.h | 8 ++ src/webui/webapplication.cpp | 36 +++-- src/webui/webapplication.h | 3 + src/webui/webui.cpp | 63 ++++++--- src/webui/webui.h | 10 +- 21 files changed, 378 insertions(+), 254 deletions(-) diff --git a/src/app/application.cpp b/src/app/application.cpp index f70293031..e22f2f670 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -96,7 +96,6 @@ #include "gui/mainwindow.h" #include "gui/shutdownconfirmdialog.h" #include "gui/uithememanager.h" -#include "gui/utils.h" #include "gui/windowstate.h" #ifdef Q_OS_WIN @@ -106,6 +105,9 @@ #ifndef DISABLE_WEBUI #include "webui/webui.h" +#ifdef DISABLE_GUI +#include "base/utils/password.h" +#endif #endif namespace @@ -310,8 +312,8 @@ Application::Application(int &argc, char **argv) if (isFileLoggerEnabled()) m_fileLogger = new FileLogger(fileLoggerPath(), isFileLoggerBackup(), fileLoggerMaxSize(), isFileLoggerDeleteOld(), fileLoggerAge(), static_cast(fileLoggerAgeType())); - if (m_commandLineArgs.webUiPort > 0) // it will be -1 when user did not set any value - Preferences::instance()->setWebUiPort(m_commandLineArgs.webUiPort); + if (m_commandLineArgs.webUIPort > 0) // it will be -1 when user did not set any value + Preferences::instance()->setWebUIPort(m_commandLineArgs.webUIPort); if (m_commandLineArgs.torrentingPort > 0) // it will be -1 when user did not set any value { @@ -899,25 +901,28 @@ int Application::exec() #endif // DISABLE_GUI #ifndef DISABLE_WEBUI +#ifndef DISABLE_GUI m_webui = new WebUI(this); -#ifdef DISABLE_GUI +#else + const Preferences *pref = Preferences::instance(); + const QString tempPassword = pref->getWebUIPassword().isEmpty() + ? Utils::Password::generate() : QString(); + m_webui = new WebUI(this, (!tempPassword.isEmpty() ? Utils::Password::PBKDF2::generate(tempPassword) : QByteArray())); if (m_webui->isErrored()) QCoreApplication::exit(EXIT_FAILURE); connect(m_webui, &WebUI::fatalError, this, []() { QCoreApplication::exit(EXIT_FAILURE); }); - const Preferences *pref = Preferences::instance(); - - const auto scheme = pref->isWebUiHttpsEnabled() ? u"https"_s : u"http"_s; - const auto url = u"%1://localhost:%2\n"_s.arg(scheme, QString::number(pref->getWebUiPort())); + const auto scheme = pref->isWebUIHttpsEnabled() ? u"https"_s : u"http"_s; + const auto url = u"%1://localhost:%2\n"_s.arg(scheme, QString::number(pref->getWebUIPort())); const QString mesg = u"\n******** %1 ********\n"_s.arg(tr("Information")) + tr("To control qBittorrent, access the WebUI at: %1").arg(url); printf("%s\n", qUtf8Printable(mesg)); - if (pref->getWebUIPassword() == QByteArrayLiteral("ARQ77eY1NUZaQsuDHbIMCA==:0WMRkYTUWVT9wVvdDtHAjU9b3b7uB8NR1Gur2hmQCvCDpm39Q+PsJRJPaCU51dEiz+dTzh8qbPsL8WkFljQYFQ==")) + if (!tempPassword.isEmpty()) { - const QString warning = tr("The Web UI administrator username is: %1").arg(pref->getWebUiUsername()) + u'\n' - + tr("The Web UI administrator password has not been changed from the default: %1").arg(u"adminadmin"_s) + u'\n' - + tr("This is a security risk, please change your password in program preferences.") + u'\n'; + const QString warning = tr("The WebUI administrator username is: %1").arg(pref->getWebUIUsername()) + u'\n' + + tr("The WebUI administrator password was not set. A temporary password is provided for this session: %1").arg(tempPassword) + u'\n' + + tr("You should set your own password in program preferences.") + u'\n'; printf("%s", qUtf8Printable(warning)); } #endif // DISABLE_GUI @@ -1300,3 +1305,10 @@ void Application::cleanup() Utils::Misc::shutdownComputer(m_shutdownAct); } } + +#ifndef DISABLE_WEBUI +WebUI *Application::webUI() const +{ + return m_webui; +} +#endif diff --git a/src/app/application.h b/src/app/application.h index 76a12ffb3..6d73c8c9a 100644 --- a/src/app/application.h +++ b/src/app/application.h @@ -149,6 +149,10 @@ private slots: #endif private: +#ifndef DISABLE_WEBUI + WebUI *webUI() const override; +#endif + void initializeTranslation(); void processParams(const QBtCommandLineParameters ¶ms); void runExternalProgram(const QString &programTemplate, const BitTorrent::Torrent *torrent) const; diff --git a/src/app/cmdoptions.cpp b/src/app/cmdoptions.cpp index 9b3dddbfa..6601f1edc 100644 --- a/src/app/cmdoptions.cpp +++ b/src/app/cmdoptions.cpp @@ -349,7 +349,7 @@ QBtCommandLineParameters::QBtCommandLineParameters(const QProcessEnvironment &en #elif !defined(Q_OS_WIN) , shouldDaemonize(DAEMON_OPTION.value(env)) #endif - , webUiPort(WEBUI_PORT_OPTION.value(env, -1)) + , webUIPort(WEBUI_PORT_OPTION.value(env, -1)) , torrentingPort(TORRENTING_PORT_OPTION.value(env, -1)) , skipDialog(SKIP_DIALOG_OPTION.value(env)) , profileDir(PROFILE_OPTION.value(env)) @@ -387,8 +387,8 @@ QBtCommandLineParameters parseCommandLine(const QStringList &args) #endif else if (arg == WEBUI_PORT_OPTION) { - result.webUiPort = WEBUI_PORT_OPTION.value(arg); - if ((result.webUiPort < 1) || (result.webUiPort > 65535)) + result.webUIPort = WEBUI_PORT_OPTION.value(arg); + if ((result.webUIPort < 1) || (result.webUIPort > 65535)) throw CommandLineParameterError(QCoreApplication::translate("CMD Options", "%1 must specify a valid port (1 to 65535).") .arg(u"--webui-port"_s)); } @@ -509,7 +509,7 @@ QString makeUsage(const QString &prgName) #endif + SHOW_HELP_OPTION.usage() + wrapText(QCoreApplication::translate("CMD Options", "Display this help message and exit")) + u'\n' + WEBUI_PORT_OPTION.usage(QCoreApplication::translate("CMD Options", "port")) - + wrapText(QCoreApplication::translate("CMD Options", "Change the Web UI port")) + + wrapText(QCoreApplication::translate("CMD Options", "Change the WebUI port")) + u'\n' + TORRENTING_PORT_OPTION.usage(QCoreApplication::translate("CMD Options", "port")) + wrapText(QCoreApplication::translate("CMD Options", "Change the torrenting port")) diff --git a/src/app/cmdoptions.h b/src/app/cmdoptions.h index 8817f4708..1c133c413 100644 --- a/src/app/cmdoptions.h +++ b/src/app/cmdoptions.h @@ -53,7 +53,7 @@ struct QBtCommandLineParameters #elif !defined(Q_OS_WIN) bool shouldDaemonize = false; #endif - int webUiPort = -1; + int webUIPort = -1; int torrentingPort = -1; std::optional skipDialog; Path profileDir; diff --git a/src/base/interfaces/iapplication.h b/src/base/interfaces/iapplication.h index 3942fd9d0..a020692f8 100644 --- a/src/base/interfaces/iapplication.h +++ b/src/base/interfaces/iapplication.h @@ -36,6 +36,7 @@ class QString; class Path; +class WebUI; struct QBtCommandLineParameters; #ifdef Q_OS_WIN @@ -83,4 +84,8 @@ public: virtual MemoryPriority processMemoryPriority() const = 0; virtual void setProcessMemoryPriority(MemoryPriority priority) = 0; #endif + +#ifndef DISABLE_WEBUI + virtual WebUI *webUI() const = 0; +#endif }; diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index ca2b101ab..4555247fb 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -628,7 +628,7 @@ void Preferences::setSearchEnabled(const bool enabled) setValue(u"Preferences/Search/SearchEnabled"_s, enabled); } -bool Preferences::isWebUiEnabled() const +bool Preferences::isWebUIEnabled() const { #ifdef DISABLE_GUI const bool defaultValue = true; @@ -638,41 +638,41 @@ bool Preferences::isWebUiEnabled() const return value(u"Preferences/WebUI/Enabled"_s, defaultValue); } -void Preferences::setWebUiEnabled(const bool enabled) +void Preferences::setWebUIEnabled(const bool enabled) { - if (enabled == isWebUiEnabled()) + if (enabled == isWebUIEnabled()) return; setValue(u"Preferences/WebUI/Enabled"_s, enabled); } -bool Preferences::isWebUiLocalAuthEnabled() const +bool Preferences::isWebUILocalAuthEnabled() const { return value(u"Preferences/WebUI/LocalHostAuth"_s, true); } -void Preferences::setWebUiLocalAuthEnabled(const bool enabled) +void Preferences::setWebUILocalAuthEnabled(const bool enabled) { - if (enabled == isWebUiLocalAuthEnabled()) + if (enabled == isWebUILocalAuthEnabled()) return; setValue(u"Preferences/WebUI/LocalHostAuth"_s, enabled); } -bool Preferences::isWebUiAuthSubnetWhitelistEnabled() const +bool Preferences::isWebUIAuthSubnetWhitelistEnabled() const { return value(u"Preferences/WebUI/AuthSubnetWhitelistEnabled"_s, false); } -void Preferences::setWebUiAuthSubnetWhitelistEnabled(const bool enabled) +void Preferences::setWebUIAuthSubnetWhitelistEnabled(const bool enabled) { - if (enabled == isWebUiAuthSubnetWhitelistEnabled()) + if (enabled == isWebUIAuthSubnetWhitelistEnabled()) return; setValue(u"Preferences/WebUI/AuthSubnetWhitelistEnabled"_s, enabled); } -QVector Preferences::getWebUiAuthSubnetWhitelist() const +QVector Preferences::getWebUIAuthSubnetWhitelist() const { const auto subnets = value(u"Preferences/WebUI/AuthSubnetWhitelist"_s); @@ -689,7 +689,7 @@ QVector Preferences::getWebUiAuthSubnetWhitelist() const return ret; } -void Preferences::setWebUiAuthSubnetWhitelist(QStringList subnets) +void Preferences::setWebUIAuthSubnetWhitelist(QStringList subnets) { Algorithm::removeIf(subnets, [](const QString &subnet) { @@ -712,27 +712,27 @@ void Preferences::setServerDomains(const QString &str) setValue(u"Preferences/WebUI/ServerDomains"_s, str); } -QString Preferences::getWebUiAddress() const +QString Preferences::getWebUIAddress() const { return value(u"Preferences/WebUI/Address"_s, u"*"_s).trimmed(); } -void Preferences::setWebUiAddress(const QString &addr) +void Preferences::setWebUIAddress(const QString &addr) { - if (addr == getWebUiAddress()) + if (addr == getWebUIAddress()) return; setValue(u"Preferences/WebUI/Address"_s, addr.trimmed()); } -quint16 Preferences::getWebUiPort() const +quint16 Preferences::getWebUIPort() const { return value(u"Preferences/WebUI/Port"_s, 8080); } -void Preferences::setWebUiPort(const quint16 port) +void Preferences::setWebUIPort(const quint16 port) { - if (port == getWebUiPort()) + if (port == getWebUIPort()) return; // cast to `int` type so it will show human readable unit in configuration file @@ -752,14 +752,14 @@ void Preferences::setUPnPForWebUIPort(const bool enabled) setValue(u"Preferences/WebUI/UseUPnP"_s, enabled); } -QString Preferences::getWebUiUsername() const +QString Preferences::getWebUIUsername() const { return value(u"Preferences/WebUI/Username"_s, u"admin"_s); } -void Preferences::setWebUiUsername(const QString &username) +void Preferences::setWebUIUsername(const QString &username) { - if (username == getWebUiUsername()) + if (username == getWebUIUsername()) return; setValue(u"Preferences/WebUI/Username"_s, username); @@ -767,9 +767,7 @@ void Preferences::setWebUiUsername(const QString &username) QByteArray Preferences::getWebUIPassword() const { - // default: adminadmin - const auto defaultValue = QByteArrayLiteral("ARQ77eY1NUZaQsuDHbIMCA==:0WMRkYTUWVT9wVvdDtHAjU9b3b7uB8NR1Gur2hmQCvCDpm39Q+PsJRJPaCU51dEiz+dTzh8qbPsL8WkFljQYFQ=="); - return value(u"Preferences/WebUI/Password_PBKDF2"_s, defaultValue); + return value(u"Preferences/WebUI/Password_PBKDF2"_s); } void Preferences::setWebUIPassword(const QByteArray &password) @@ -832,40 +830,40 @@ void Preferences::setWebAPISessionCookieName(const QString &cookieName) setValue(u"WebAPI/SessionCookieName"_s, cookieName); } -bool Preferences::isWebUiClickjackingProtectionEnabled() const +bool Preferences::isWebUIClickjackingProtectionEnabled() const { return value(u"Preferences/WebUI/ClickjackingProtection"_s, true); } -void Preferences::setWebUiClickjackingProtectionEnabled(const bool enabled) +void Preferences::setWebUIClickjackingProtectionEnabled(const bool enabled) { - if (enabled == isWebUiClickjackingProtectionEnabled()) + if (enabled == isWebUIClickjackingProtectionEnabled()) return; setValue(u"Preferences/WebUI/ClickjackingProtection"_s, enabled); } -bool Preferences::isWebUiCSRFProtectionEnabled() const +bool Preferences::isWebUICSRFProtectionEnabled() const { return value(u"Preferences/WebUI/CSRFProtection"_s, true); } -void Preferences::setWebUiCSRFProtectionEnabled(const bool enabled) +void Preferences::setWebUICSRFProtectionEnabled(const bool enabled) { - if (enabled == isWebUiCSRFProtectionEnabled()) + if (enabled == isWebUICSRFProtectionEnabled()) return; setValue(u"Preferences/WebUI/CSRFProtection"_s, enabled); } -bool Preferences::isWebUiSecureCookieEnabled() const +bool Preferences::isWebUISecureCookieEnabled() const { return value(u"Preferences/WebUI/SecureCookie"_s, true); } -void Preferences::setWebUiSecureCookieEnabled(const bool enabled) +void Preferences::setWebUISecureCookieEnabled(const bool enabled) { - if (enabled == isWebUiSecureCookieEnabled()) + if (enabled == isWebUISecureCookieEnabled()) return; setValue(u"Preferences/WebUI/SecureCookie"_s, enabled); @@ -884,14 +882,14 @@ void Preferences::setWebUIHostHeaderValidationEnabled(const bool enabled) setValue(u"Preferences/WebUI/HostHeaderValidation"_s, enabled); } -bool Preferences::isWebUiHttpsEnabled() const +bool Preferences::isWebUIHttpsEnabled() const { return value(u"Preferences/WebUI/HTTPS/Enabled"_s, false); } -void Preferences::setWebUiHttpsEnabled(const bool enabled) +void Preferences::setWebUIHttpsEnabled(const bool enabled) { - if (enabled == isWebUiHttpsEnabled()) + if (enabled == isWebUIHttpsEnabled()) return; setValue(u"Preferences/WebUI/HTTPS/Enabled"_s, enabled); @@ -923,27 +921,27 @@ void Preferences::setWebUIHttpsKeyPath(const Path &path) setValue(u"Preferences/WebUI/HTTPS/KeyPath"_s, path); } -bool Preferences::isAltWebUiEnabled() const +bool Preferences::isAltWebUIEnabled() const { return value(u"Preferences/WebUI/AlternativeUIEnabled"_s, false); } -void Preferences::setAltWebUiEnabled(const bool enabled) +void Preferences::setAltWebUIEnabled(const bool enabled) { - if (enabled == isAltWebUiEnabled()) + if (enabled == isAltWebUIEnabled()) return; setValue(u"Preferences/WebUI/AlternativeUIEnabled"_s, enabled); } -Path Preferences::getWebUiRootFolder() const +Path Preferences::getWebUIRootFolder() const { return value(u"Preferences/WebUI/RootFolder"_s); } -void Preferences::setWebUiRootFolder(const Path &path) +void Preferences::setWebUIRootFolder(const Path &path) { - if (path == getWebUiRootFolder()) + if (path == getWebUIRootFolder()) return; setValue(u"Preferences/WebUI/RootFolder"_s, path); diff --git a/src/base/preferences.h b/src/base/preferences.h index 3cd35c8c6..fe19d930e 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -169,26 +169,26 @@ public: void setSearchEnabled(bool enabled); // HTTP Server - bool isWebUiEnabled() const; - void setWebUiEnabled(bool enabled); + bool isWebUIEnabled() const; + void setWebUIEnabled(bool enabled); QString getServerDomains() const; void setServerDomains(const QString &str); - QString getWebUiAddress() const; - void setWebUiAddress(const QString &addr); - quint16 getWebUiPort() const; - void setWebUiPort(quint16 port); + QString getWebUIAddress() const; + void setWebUIAddress(const QString &addr); + quint16 getWebUIPort() const; + void setWebUIPort(quint16 port); bool useUPnPForWebUIPort() const; void setUPnPForWebUIPort(bool enabled); // Authentication - bool isWebUiLocalAuthEnabled() const; - void setWebUiLocalAuthEnabled(bool enabled); - bool isWebUiAuthSubnetWhitelistEnabled() const; - void setWebUiAuthSubnetWhitelistEnabled(bool enabled); - QVector getWebUiAuthSubnetWhitelist() const; - void setWebUiAuthSubnetWhitelist(QStringList subnets); - QString getWebUiUsername() const; - void setWebUiUsername(const QString &username); + bool isWebUILocalAuthEnabled() const; + void setWebUILocalAuthEnabled(bool enabled); + bool isWebUIAuthSubnetWhitelistEnabled() const; + void setWebUIAuthSubnetWhitelistEnabled(bool enabled); + QVector getWebUIAuthSubnetWhitelist() const; + void setWebUIAuthSubnetWhitelist(QStringList subnets); + QString getWebUIUsername() const; + void setWebUIUsername(const QString &username); QByteArray getWebUIPassword() const; void setWebUIPassword(const QByteArray &password); int getWebUIMaxAuthFailCount() const; @@ -201,26 +201,26 @@ public: void setWebAPISessionCookieName(const QString &cookieName); // WebUI security - bool isWebUiClickjackingProtectionEnabled() const; - void setWebUiClickjackingProtectionEnabled(bool enabled); - bool isWebUiCSRFProtectionEnabled() const; - void setWebUiCSRFProtectionEnabled(bool enabled); - bool isWebUiSecureCookieEnabled () const; - void setWebUiSecureCookieEnabled(bool enabled); + bool isWebUIClickjackingProtectionEnabled() const; + void setWebUIClickjackingProtectionEnabled(bool enabled); + bool isWebUICSRFProtectionEnabled() const; + void setWebUICSRFProtectionEnabled(bool enabled); + bool isWebUISecureCookieEnabled () const; + void setWebUISecureCookieEnabled(bool enabled); bool isWebUIHostHeaderValidationEnabled() const; void setWebUIHostHeaderValidationEnabled(bool enabled); // HTTPS - bool isWebUiHttpsEnabled() const; - void setWebUiHttpsEnabled(bool enabled); + bool isWebUIHttpsEnabled() const; + void setWebUIHttpsEnabled(bool enabled); Path getWebUIHttpsCertificatePath() const; void setWebUIHttpsCertificatePath(const Path &path); Path getWebUIHttpsKeyPath() const; void setWebUIHttpsKeyPath(const Path &path); - bool isAltWebUiEnabled() const; - void setAltWebUiEnabled(bool enabled); - Path getWebUiRootFolder() const; - void setWebUiRootFolder(const Path &path); + bool isAltWebUIEnabled() const; + void setAltWebUIEnabled(bool enabled); + Path getWebUIRootFolder() const; + void setWebUIRootFolder(const Path &path); // WebUI custom HTTP headers bool isWebUICustomHTTPHeadersEnabled() const; diff --git a/src/base/utils/password.cpp b/src/base/utils/password.cpp index 0351aff64..4ed8989ad 100644 --- a/src/base/utils/password.cpp +++ b/src/base/utils/password.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2018 Mike Tzou (Chocobo1) * * This program is free software; you can redistribute it and/or @@ -36,6 +37,7 @@ #include #include +#include "base/global.h" #include "bytearray.h" #include "random.h" @@ -65,6 +67,21 @@ bool Utils::Password::slowEquals(const QByteArray &a, const QByteArray &b) return (diff == 0); } +QString Utils::Password::generate() +{ + const QString alphanum = u"23456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"_s; + const int passwordLength = 9; + QString pass; + pass.reserve(passwordLength); + while (pass.length() < passwordLength) + { + const auto num = Utils::Random::rand(0, (alphanum.size() - 1)); + pass.append(alphanum[num]); + } + + return pass; +} + QByteArray Utils::Password::PBKDF2::generate(const QString &password) { return generate(password.toUtf8()); @@ -72,9 +89,8 @@ QByteArray Utils::Password::PBKDF2::generate(const QString &password) QByteArray Utils::Password::PBKDF2::generate(const QByteArray &password) { - const std::array salt - {{Random::rand(), Random::rand() - , Random::rand(), Random::rand()}}; + const std::array salt { + {Random::rand(), Random::rand(), Random::rand(), Random::rand()}}; std::array outBuf {}; const int hmacResult = PKCS5_PBKDF2_HMAC(password.constData(), password.size() diff --git a/src/base/utils/password.h b/src/base/utils/password.h index 3ad6d8578..b656731e9 100644 --- a/src/base/utils/password.h +++ b/src/base/utils/password.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2018 Mike Tzou (Chocobo1) * * This program is free software; you can redistribute it and/or @@ -37,6 +38,8 @@ namespace Utils::Password // Taken from https://crackstation.net/hashing-security.htm bool slowEquals(const QByteArray &a, const QByteArray &b); + QString generate(); + namespace PBKDF2 { QByteArray generate(const QString &password); diff --git a/src/gui/ipsubnetwhitelistoptionsdialog.cpp b/src/gui/ipsubnetwhitelistoptionsdialog.cpp index 78af3610f..11556422c 100644 --- a/src/gui/ipsubnetwhitelistoptionsdialog.cpp +++ b/src/gui/ipsubnetwhitelistoptionsdialog.cpp @@ -50,7 +50,7 @@ IPSubnetWhitelistOptionsDialog::IPSubnetWhitelistOptionsDialog(QWidget *parent) connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QStringList authSubnetWhitelistStringList; - for (const Utils::Net::Subnet &subnet : asConst(Preferences::instance()->getWebUiAuthSubnetWhitelist())) + for (const Utils::Net::Subnet &subnet : asConst(Preferences::instance()->getWebUIAuthSubnetWhitelist())) authSubnetWhitelistStringList << Utils::Net::subnetToString(subnet); m_model = new QStringListModel(authSubnetWhitelistStringList, this); @@ -81,7 +81,7 @@ void IPSubnetWhitelistOptionsDialog::on_buttonBox_accepted() // Operate on the m_sortFilter to grab the strings in sorted order for (int i = 0; i < m_sortFilter->rowCount(); ++i) subnets.append(m_sortFilter->index(i, 0).data().toString()); - Preferences::instance()->setWebUiAuthSubnetWhitelist(subnets); + Preferences::instance()->setWebUIAuthSubnetWhitelist(subnets); QDialog::accept(); } else diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 8a10f7025..51a607625 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -1182,7 +1182,7 @@ void MainWindow::closeEvent(QCloseEvent *e) if (!isVisible()) show(); QMessageBox confirmBox(QMessageBox::Question, tr("Exiting qBittorrent"), - // Split it because the last sentence is used in the Web UI + // Split it because the last sentence is used in the WebUI tr("Some files are currently transferring.") + u'\n' + tr("Are you sure you want to quit qBittorrent?"), QMessageBox::NoButton, this); QPushButton *noBtn = confirmBox.addButton(tr("&No"), QMessageBox::NoRole); diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index 4247c4e8d..0c20158f3 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -69,6 +69,7 @@ #include "utils.h" #include "watchedfolderoptionsdialog.h" #include "watchedfoldersmodel.h" +#include "webui/webui.h" #ifndef DISABLE_WEBUI #include "base/net/dnsupdater.h" @@ -80,6 +81,9 @@ #define SETTINGS_KEY(name) u"OptionsDialog/" name +const int WEBUI_MIN_USERNAME_LENGTH = 3; +const int WEBUI_MIN_PASSWORD_LENGTH = 6; + namespace { QStringList translatedWeekdayNames() @@ -106,6 +110,16 @@ namespace } }; + bool isValidWebUIUsername(const QString &username) + { + return (username.length() >= WEBUI_MIN_USERNAME_LENGTH); + } + + bool isValidWebUIPassword(const QString &password) + { + return (password.length() >= WEBUI_MIN_PASSWORD_LENGTH); + } + // Shortcuts for frequently used signals that have more than one overload. They would require // type casts and that is why we declare required member pointer here instead. void (QComboBox::*qComboBoxCurrentIndexChanged)(int) = &QComboBox::currentIndexChanged; @@ -175,7 +189,11 @@ OptionsDialog::OptionsDialog(IGUIApplication *app, QWidget *parent) // setup apply button m_applyButton->setEnabled(false); - connect(m_applyButton, &QPushButton::clicked, this, &OptionsDialog::applySettings); + connect(m_applyButton, &QPushButton::clicked, this, [this] + { + if (applySettings()) + m_applyButton->setEnabled(false); + }); // disable mouse wheel event on widgets to avoid misselection auto *wheelEventEater = new WheelEventEater(this); @@ -1211,28 +1229,33 @@ void OptionsDialog::loadWebUITabOptions() m_ui->textWebUIRootFolder->setMode(FileSystemPathEdit::Mode::DirectoryOpen); m_ui->textWebUIRootFolder->setDialogCaption(tr("Choose Alternative UI files location")); - m_ui->checkWebUi->setChecked(pref->isWebUiEnabled()); - m_ui->textWebUiAddress->setText(pref->getWebUiAddress()); - m_ui->spinWebUiPort->setValue(pref->getWebUiPort()); + if (app()->webUI()->isErrored()) + m_ui->labelWebUIError->setText(tr("WebUI configuration failed. Reason: %1").arg(app()->webUI()->errorMessage())); + else + m_ui->labelWebUIError->hide(); + + m_ui->checkWebUI->setChecked(pref->isWebUIEnabled()); + m_ui->textWebUIAddress->setText(pref->getWebUIAddress()); + m_ui->spinWebUIPort->setValue(pref->getWebUIPort()); m_ui->checkWebUIUPnP->setChecked(pref->useUPnPForWebUIPort()); - m_ui->checkWebUiHttps->setChecked(pref->isWebUiHttpsEnabled()); + m_ui->checkWebUIHttps->setChecked(pref->isWebUIHttpsEnabled()); webUIHttpsCertChanged(pref->getWebUIHttpsCertificatePath()); webUIHttpsKeyChanged(pref->getWebUIHttpsKeyPath()); - m_ui->textWebUiUsername->setText(pref->getWebUiUsername()); - m_ui->checkBypassLocalAuth->setChecked(!pref->isWebUiLocalAuthEnabled()); - m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUiAuthSubnetWhitelistEnabled()); + m_ui->textWebUIUsername->setText(pref->getWebUIUsername()); + m_ui->checkBypassLocalAuth->setChecked(!pref->isWebUILocalAuthEnabled()); + m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUIAuthSubnetWhitelistEnabled()); m_ui->IPSubnetWhitelistButton->setEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked()); m_ui->spinBanCounter->setValue(pref->getWebUIMaxAuthFailCount()); m_ui->spinBanDuration->setValue(pref->getWebUIBanDuration().count()); m_ui->spinSessionTimeout->setValue(pref->getWebUISessionTimeout()); // Alternative UI - m_ui->groupAltWebUI->setChecked(pref->isAltWebUiEnabled()); - m_ui->textWebUIRootFolder->setSelectedPath(pref->getWebUiRootFolder()); + m_ui->groupAltWebUI->setChecked(pref->isAltWebUIEnabled()); + m_ui->textWebUIRootFolder->setSelectedPath(pref->getWebUIRootFolder()); // Security - m_ui->checkClickjacking->setChecked(pref->isWebUiClickjackingProtectionEnabled()); - m_ui->checkCSRFProtection->setChecked(pref->isWebUiCSRFProtectionEnabled()); - m_ui->checkSecureCookie->setEnabled(pref->isWebUiHttpsEnabled()); - m_ui->checkSecureCookie->setChecked(pref->isWebUiSecureCookieEnabled()); + m_ui->checkClickjacking->setChecked(pref->isWebUIClickjackingProtectionEnabled()); + m_ui->checkCSRFProtection->setChecked(pref->isWebUICSRFProtectionEnabled()); + m_ui->checkSecureCookie->setEnabled(pref->isWebUIHttpsEnabled()); + m_ui->checkSecureCookie->setChecked(pref->isWebUISecureCookieEnabled()); m_ui->groupHostHeaderValidation->setChecked(pref->isWebUIHostHeaderValidationEnabled()); m_ui->textServerDomains->setText(pref->getServerDomains()); // Custom HTTP headers @@ -1248,18 +1271,18 @@ void OptionsDialog::loadWebUITabOptions() m_ui->DNSUsernameTxt->setText(pref->getDynDNSUsername()); m_ui->DNSPasswordTxt->setText(pref->getDynDNSPassword()); - connect(m_ui->checkWebUi, &QGroupBox::toggled, this, &ThisType::enableApplyButton); - connect(m_ui->textWebUiAddress, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); - connect(m_ui->spinWebUiPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); + connect(m_ui->checkWebUI, &QGroupBox::toggled, this, &ThisType::enableApplyButton); + connect(m_ui->textWebUIAddress, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); + connect(m_ui->spinWebUIPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); connect(m_ui->checkWebUIUPnP, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); - connect(m_ui->checkWebUiHttps, &QGroupBox::toggled, this, &ThisType::enableApplyButton); + connect(m_ui->checkWebUIHttps, &QGroupBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->textWebUIHttpsCert, &FileSystemPathLineEdit::selectedPathChanged, this, &ThisType::enableApplyButton); connect(m_ui->textWebUIHttpsCert, &FileSystemPathLineEdit::selectedPathChanged, this, &OptionsDialog::webUIHttpsCertChanged); connect(m_ui->textWebUIHttpsKey, &FileSystemPathLineEdit::selectedPathChanged, this, &ThisType::enableApplyButton); connect(m_ui->textWebUIHttpsKey, &FileSystemPathLineEdit::selectedPathChanged, this, &OptionsDialog::webUIHttpsKeyChanged); - connect(m_ui->textWebUiUsername, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); - connect(m_ui->textWebUiPassword, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); + connect(m_ui->textWebUIUsername, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); + connect(m_ui->textWebUIPassword, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->checkBypassLocalAuth, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkBypassAuthSubnetWhitelist, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); @@ -1273,7 +1296,7 @@ void OptionsDialog::loadWebUITabOptions() connect(m_ui->checkClickjacking, &QCheckBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkCSRFProtection, &QCheckBox::toggled, this, &ThisType::enableApplyButton); - connect(m_ui->checkWebUiHttps, &QGroupBox::toggled, m_ui->checkSecureCookie, &QWidget::setEnabled); + connect(m_ui->checkWebUIHttps, &QGroupBox::toggled, m_ui->checkSecureCookie, &QWidget::setEnabled); connect(m_ui->checkSecureCookie, &QCheckBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->groupHostHeaderValidation, &QGroupBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->textServerDomains, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); @@ -1295,29 +1318,32 @@ void OptionsDialog::saveWebUITabOptions() const { auto *pref = Preferences::instance(); - pref->setWebUiEnabled(isWebUiEnabled()); - pref->setWebUiAddress(m_ui->textWebUiAddress->text()); - pref->setWebUiPort(m_ui->spinWebUiPort->value()); + const bool webUIEnabled = isWebUIEnabled(); + + pref->setWebUIEnabled(webUIEnabled); + pref->setWebUIAddress(m_ui->textWebUIAddress->text()); + pref->setWebUIPort(m_ui->spinWebUIPort->value()); pref->setUPnPForWebUIPort(m_ui->checkWebUIUPnP->isChecked()); - pref->setWebUiHttpsEnabled(m_ui->checkWebUiHttps->isChecked()); + pref->setWebUIHttpsEnabled(m_ui->checkWebUIHttps->isChecked()); pref->setWebUIHttpsCertificatePath(m_ui->textWebUIHttpsCert->selectedPath()); pref->setWebUIHttpsKeyPath(m_ui->textWebUIHttpsKey->selectedPath()); pref->setWebUIMaxAuthFailCount(m_ui->spinBanCounter->value()); pref->setWebUIBanDuration(std::chrono::seconds {m_ui->spinBanDuration->value()}); pref->setWebUISessionTimeout(m_ui->spinSessionTimeout->value()); // Authentication - pref->setWebUiUsername(webUiUsername()); - if (!webUiPassword().isEmpty()) - pref->setWebUIPassword(Utils::Password::PBKDF2::generate(webUiPassword())); - pref->setWebUiLocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked()); - pref->setWebUiAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked()); + if (const QString username = webUIUsername(); isValidWebUIUsername(username)) + pref->setWebUIUsername(username); + if (const QString password = webUIPassword(); isValidWebUIPassword(password)) + pref->setWebUIPassword(Utils::Password::PBKDF2::generate(password)); + pref->setWebUILocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked()); + pref->setWebUIAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked()); // Alternative UI - pref->setAltWebUiEnabled(m_ui->groupAltWebUI->isChecked()); - pref->setWebUiRootFolder(m_ui->textWebUIRootFolder->selectedPath()); + pref->setAltWebUIEnabled(m_ui->groupAltWebUI->isChecked()); + pref->setWebUIRootFolder(m_ui->textWebUIRootFolder->selectedPath()); // Security - pref->setWebUiClickjackingProtectionEnabled(m_ui->checkClickjacking->isChecked()); - pref->setWebUiCSRFProtectionEnabled(m_ui->checkCSRFProtection->isChecked()); - pref->setWebUiSecureCookieEnabled(m_ui->checkSecureCookie->isChecked()); + pref->setWebUIClickjackingProtectionEnabled(m_ui->checkClickjacking->isChecked()); + pref->setWebUICSRFProtectionEnabled(m_ui->checkCSRFProtection->isChecked()); + pref->setWebUISecureCookieEnabled(m_ui->checkSecureCookie->isChecked()); pref->setWebUIHostHeaderValidationEnabled(m_ui->groupHostHeaderValidation->isChecked()); pref->setServerDomains(m_ui->textServerDomains->text()); // Custom HTTP headers @@ -1517,53 +1543,37 @@ void OptionsDialog::on_buttonBox_accepted() { if (m_applyButton->isEnabled()) { - if (!schedTimesOk()) - { - m_ui->tabSelection->setCurrentRow(TAB_SPEED); + if (!applySettings()) return; - } -#ifndef DISABLE_WEBUI - if (!webUIAuthenticationOk()) - { - m_ui->tabSelection->setCurrentRow(TAB_WEBUI); - return; - } - if (!isAlternativeWebUIPathValid()) - { - m_ui->tabSelection->setCurrentRow(TAB_WEBUI); - return; - } -#endif m_applyButton->setEnabled(false); - saveOptions(); } accept(); } -void OptionsDialog::applySettings() +bool OptionsDialog::applySettings() { if (!schedTimesOk()) { m_ui->tabSelection->setCurrentRow(TAB_SPEED); - return; + return false; } #ifndef DISABLE_WEBUI - if (!webUIAuthenticationOk()) + if (isWebUIEnabled() && !webUIAuthenticationOk()) { m_ui->tabSelection->setCurrentRow(TAB_WEBUI); - return; + return false; } if (!isAlternativeWebUIPathValid()) { m_ui->tabSelection->setCurrentRow(TAB_WEBUI); - return; + return false; } #endif - m_applyButton->setEnabled(false); saveOptions(); + return true; } void OptionsDialog::on_buttonBox_rejected() @@ -1859,31 +1869,33 @@ void OptionsDialog::webUIHttpsKeyChanged(const Path &path) (isKeyValid ? u"security-high"_s : u"security-low"_s), 24)); } -bool OptionsDialog::isWebUiEnabled() const +bool OptionsDialog::isWebUIEnabled() const { - return m_ui->checkWebUi->isChecked(); + return m_ui->checkWebUI->isChecked(); } -QString OptionsDialog::webUiUsername() const +QString OptionsDialog::webUIUsername() const { - return m_ui->textWebUiUsername->text(); + return m_ui->textWebUIUsername->text(); } -QString OptionsDialog::webUiPassword() const +QString OptionsDialog::webUIPassword() const { - return m_ui->textWebUiPassword->text(); + return m_ui->textWebUIPassword->text(); } bool OptionsDialog::webUIAuthenticationOk() { - if (webUiUsername().length() < 3) + if (!isValidWebUIUsername(webUIUsername())) { - QMessageBox::warning(this, tr("Length Error"), tr("The Web UI username must be at least 3 characters long.")); + QMessageBox::warning(this, tr("Length Error"), tr("The WebUI username must be at least 3 characters long.")); return false; } - if (!webUiPassword().isEmpty() && (webUiPassword().length() < 6)) + + const bool dontChangePassword = webUIPassword().isEmpty() && !Preferences::instance()->getWebUIPassword().isEmpty(); + if (!isValidWebUIPassword(webUIPassword()) && !dontChangePassword) { - QMessageBox::warning(this, tr("Length Error"), tr("The Web UI password must be at least 6 characters long.")); + QMessageBox::warning(this, tr("Length Error"), tr("The WebUI password must be at least 6 characters long.")); return false; } return true; @@ -1893,7 +1905,7 @@ bool OptionsDialog::isAlternativeWebUIPathValid() { if (m_ui->groupAltWebUI->isChecked() && m_ui->textWebUIRootFolder->selectedPath().isEmpty()) { - QMessageBox::warning(this, tr("Location Error"), tr("The alternative Web UI files location cannot be blank.")); + QMessageBox::warning(this, tr("Location Error"), tr("The alternative WebUI files location cannot be blank.")); return false; } return true; diff --git a/src/gui/optionsdialog.h b/src/gui/optionsdialog.h index dfb5a75c8..8bcb64744 100644 --- a/src/gui/optionsdialog.h +++ b/src/gui/optionsdialog.h @@ -88,7 +88,6 @@ private slots: void adjustProxyOptions(); void on_buttonBox_accepted(); void on_buttonBox_rejected(); - void applySettings(); void enableApplyButton(); void toggleComboRatioLimitAct(); void changePage(QListWidgetItem *, QListWidgetItem *); @@ -115,6 +114,7 @@ private: void showEvent(QShowEvent *e) override; // Methods + bool applySettings(); void saveOptions() const; void loadBehaviorTabOptions(); @@ -184,9 +184,9 @@ private: int getMaxActiveTorrents() const; // WebUI #ifndef DISABLE_WEBUI - bool isWebUiEnabled() const; - QString webUiUsername() const; - QString webUiPassword() const; + bool isWebUIEnabled() const; + QString webUIUsername() const; + QString webUIPassword() const; bool webUIAuthenticationOk(); bool isAlternativeWebUIPathValid(); #endif diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index 089cbb849..4f7e826b3 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -3223,8 +3223,8 @@ Disable encryption: Only connect to peers without protocol encryption - - + + 0 @@ -3253,7 +3253,7 @@ Disable encryption: Only connect to peers without protocol encryption - + Web User Interface (Remote control) @@ -3264,17 +3264,29 @@ Disable encryption: Only connect to peers without protocol encryption false + + + + + true + + + + + + + - + IP address: - + IP address that the Web UI will bind to. Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv4 address, @@ -3283,14 +3295,14 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv - + Port: - + 1 @@ -3315,7 +3327,7 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv - + &Use HTTPS instead of HTTP @@ -3327,14 +3339,14 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv - + Key: - + Certificate: @@ -3366,7 +3378,7 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv - + Authentication @@ -3374,24 +3386,24 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv - + Username: - + - + Password: - + QLineEdit::Password @@ -3819,13 +3831,13 @@ Use ';' to split multiple entries. Can use wildcard '*'. stopConditionComboBox spinPort checkUPnP - textWebUiUsername - checkWebUi + textWebUIUsername + checkWebUI textSavePath scrollArea_7 scrollArea_2 - spinWebUiPort - textWebUiPassword + spinWebUIPort + textWebUIPassword buttonBox tabSelection scrollArea @@ -3915,7 +3927,7 @@ Use ';' to split multiple entries. Can use wildcard '*'. spinMaxActiveUploads spinMaxActiveTorrents checkWebUIUPnP - checkWebUiHttps + checkWebUIHttps checkBypassLocalAuth checkBypassAuthSubnetWhitelist IPSubnetWhitelistButton diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index de314fd5c..cb60f96e0 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -92,7 +92,7 @@ void AppController::buildInfoAction() void AppController::shutdownAction() { // Special handling for shutdown, we - // need to reply to the Web UI before + // need to reply to the WebUI before // actually shutting down. QTimer::singleShot(100ms, Qt::CoarseTimer, qApp, [] { @@ -275,33 +275,33 @@ void AppController::preferencesAction() data[u"add_trackers_enabled"_s] = session->isAddTrackersEnabled(); data[u"add_trackers"_s] = session->additionalTrackers(); - // Web UI + // WebUI // HTTP Server data[u"web_ui_domain_list"_s] = pref->getServerDomains(); - data[u"web_ui_address"_s] = pref->getWebUiAddress(); - data[u"web_ui_port"_s] = pref->getWebUiPort(); + data[u"web_ui_address"_s] = pref->getWebUIAddress(); + data[u"web_ui_port"_s] = pref->getWebUIPort(); data[u"web_ui_upnp"_s] = pref->useUPnPForWebUIPort(); - data[u"use_https"_s] = pref->isWebUiHttpsEnabled(); + data[u"use_https"_s] = pref->isWebUIHttpsEnabled(); data[u"web_ui_https_cert_path"_s] = pref->getWebUIHttpsCertificatePath().toString(); data[u"web_ui_https_key_path"_s] = pref->getWebUIHttpsKeyPath().toString(); // Authentication - data[u"web_ui_username"_s] = pref->getWebUiUsername(); - data[u"bypass_local_auth"_s] = !pref->isWebUiLocalAuthEnabled(); - data[u"bypass_auth_subnet_whitelist_enabled"_s] = pref->isWebUiAuthSubnetWhitelistEnabled(); + data[u"web_ui_username"_s] = pref->getWebUIUsername(); + data[u"bypass_local_auth"_s] = !pref->isWebUILocalAuthEnabled(); + data[u"bypass_auth_subnet_whitelist_enabled"_s] = pref->isWebUIAuthSubnetWhitelistEnabled(); QStringList authSubnetWhitelistStringList; - for (const Utils::Net::Subnet &subnet : asConst(pref->getWebUiAuthSubnetWhitelist())) + for (const Utils::Net::Subnet &subnet : asConst(pref->getWebUIAuthSubnetWhitelist())) authSubnetWhitelistStringList << Utils::Net::subnetToString(subnet); data[u"bypass_auth_subnet_whitelist"_s] = authSubnetWhitelistStringList.join(u'\n'); data[u"web_ui_max_auth_fail_count"_s] = pref->getWebUIMaxAuthFailCount(); data[u"web_ui_ban_duration"_s] = static_cast(pref->getWebUIBanDuration().count()); data[u"web_ui_session_timeout"_s] = pref->getWebUISessionTimeout(); - // Use alternative Web UI - data[u"alternative_webui_enabled"_s] = pref->isAltWebUiEnabled(); - data[u"alternative_webui_path"_s] = pref->getWebUiRootFolder().toString(); + // Use alternative WebUI + data[u"alternative_webui_enabled"_s] = pref->isAltWebUIEnabled(); + data[u"alternative_webui_path"_s] = pref->getWebUIRootFolder().toString(); // Security - data[u"web_ui_clickjacking_protection_enabled"_s] = pref->isWebUiClickjackingProtectionEnabled(); - data[u"web_ui_csrf_protection_enabled"_s] = pref->isWebUiCSRFProtectionEnabled(); - data[u"web_ui_secure_cookie_enabled"_s] = pref->isWebUiSecureCookieEnabled(); + data[u"web_ui_clickjacking_protection_enabled"_s] = pref->isWebUIClickjackingProtectionEnabled(); + data[u"web_ui_csrf_protection_enabled"_s] = pref->isWebUICSRFProtectionEnabled(); + data[u"web_ui_secure_cookie_enabled"_s] = pref->isWebUISecureCookieEnabled(); data[u"web_ui_host_header_validation_enabled"_s] = pref->isWebUIHostHeaderValidationEnabled(); // Custom HTTP headers data[u"web_ui_use_custom_http_headers_enabled"_s] = pref->isWebUICustomHTTPHeadersEnabled(); @@ -782,35 +782,35 @@ void AppController::setPreferencesAction() if (hasKey(u"add_trackers"_s)) session->setAdditionalTrackers(it.value().toString()); - // Web UI + // WebUI // HTTP Server if (hasKey(u"web_ui_domain_list"_s)) pref->setServerDomains(it.value().toString()); if (hasKey(u"web_ui_address"_s)) - pref->setWebUiAddress(it.value().toString()); + pref->setWebUIAddress(it.value().toString()); if (hasKey(u"web_ui_port"_s)) - pref->setWebUiPort(it.value().value()); + pref->setWebUIPort(it.value().value()); if (hasKey(u"web_ui_upnp"_s)) pref->setUPnPForWebUIPort(it.value().toBool()); if (hasKey(u"use_https"_s)) - pref->setWebUiHttpsEnabled(it.value().toBool()); + pref->setWebUIHttpsEnabled(it.value().toBool()); if (hasKey(u"web_ui_https_cert_path"_s)) pref->setWebUIHttpsCertificatePath(Path(it.value().toString())); if (hasKey(u"web_ui_https_key_path"_s)) pref->setWebUIHttpsKeyPath(Path(it.value().toString())); // Authentication if (hasKey(u"web_ui_username"_s)) - pref->setWebUiUsername(it.value().toString()); + pref->setWebUIUsername(it.value().toString()); if (hasKey(u"web_ui_password"_s)) pref->setWebUIPassword(Utils::Password::PBKDF2::generate(it.value().toByteArray())); if (hasKey(u"bypass_local_auth"_s)) - pref->setWebUiLocalAuthEnabled(!it.value().toBool()); + pref->setWebUILocalAuthEnabled(!it.value().toBool()); if (hasKey(u"bypass_auth_subnet_whitelist_enabled"_s)) - pref->setWebUiAuthSubnetWhitelistEnabled(it.value().toBool()); + pref->setWebUIAuthSubnetWhitelistEnabled(it.value().toBool()); if (hasKey(u"bypass_auth_subnet_whitelist"_s)) { // recognize new lines and commas as delimiters - pref->setWebUiAuthSubnetWhitelist(it.value().toString().split(QRegularExpression(u"\n|,"_s), Qt::SkipEmptyParts)); + pref->setWebUIAuthSubnetWhitelist(it.value().toString().split(QRegularExpression(u"\n|,"_s), Qt::SkipEmptyParts)); } if (hasKey(u"web_ui_max_auth_fail_count"_s)) pref->setWebUIMaxAuthFailCount(it.value().toInt()); @@ -818,18 +818,18 @@ void AppController::setPreferencesAction() pref->setWebUIBanDuration(std::chrono::seconds {it.value().toInt()}); if (hasKey(u"web_ui_session_timeout"_s)) pref->setWebUISessionTimeout(it.value().toInt()); - // Use alternative Web UI + // Use alternative WebUI if (hasKey(u"alternative_webui_enabled"_s)) - pref->setAltWebUiEnabled(it.value().toBool()); + pref->setAltWebUIEnabled(it.value().toBool()); if (hasKey(u"alternative_webui_path"_s)) - pref->setWebUiRootFolder(Path(it.value().toString())); + pref->setWebUIRootFolder(Path(it.value().toString())); // Security if (hasKey(u"web_ui_clickjacking_protection_enabled"_s)) - pref->setWebUiClickjackingProtectionEnabled(it.value().toBool()); + pref->setWebUIClickjackingProtectionEnabled(it.value().toBool()); if (hasKey(u"web_ui_csrf_protection_enabled"_s)) - pref->setWebUiCSRFProtectionEnabled(it.value().toBool()); + pref->setWebUICSRFProtectionEnabled(it.value().toBool()); if (hasKey(u"web_ui_secure_cookie_enabled"_s)) - pref->setWebUiSecureCookieEnabled(it.value().toBool()); + pref->setWebUISecureCookieEnabled(it.value().toBool()); if (hasKey(u"web_ui_host_header_validation_enabled"_s)) pref->setWebUIHostHeaderValidationEnabled(it.value().toBool()); // Custom HTTP headers diff --git a/src/webui/api/authcontroller.cpp b/src/webui/api/authcontroller.cpp index 24b4ce78e..eb1d1baf2 100644 --- a/src/webui/api/authcontroller.cpp +++ b/src/webui/api/authcontroller.cpp @@ -43,6 +43,16 @@ AuthController::AuthController(ISessionManager *sessionManager, IApplication *ap { } +void AuthController::setUsername(const QString &username) +{ + m_username = username; +} + +void AuthController::setPasswordHash(const QByteArray &passwordHash) +{ + m_passwordHash = passwordHash; +} + void AuthController::loginAction() { if (m_sessionManager->session()) @@ -51,9 +61,9 @@ void AuthController::loginAction() return; } - const QString clientAddr {m_sessionManager->clientId()}; - const QString usernameFromWeb {params()[u"username"_s]}; - const QString passwordFromWeb {params()[u"password"_s]}; + const QString clientAddr = m_sessionManager->clientId(); + const QString usernameFromWeb = params()[u"username"_s]; + const QString passwordFromWeb = params()[u"password"_s]; if (isBanned()) { @@ -61,15 +71,11 @@ void AuthController::loginAction() .arg(clientAddr, usernameFromWeb) , Log::WARNING); throw APIError(APIErrorType::AccessDenied - , tr("Your IP address has been banned after too many failed authentication attempts.")); + , tr("Your IP address has been banned after too many failed authentication attempts.")); } - const Preferences *pref = Preferences::instance(); - - const QString username {pref->getWebUiUsername()}; - const QByteArray secret {pref->getWebUIPassword()}; - const bool usernameEqual = Utils::Password::slowEquals(usernameFromWeb.toUtf8(), username.toUtf8()); - const bool passwordEqual = Utils::Password::PBKDF2::verify(secret, passwordFromWeb); + const bool usernameEqual = Utils::Password::slowEquals(usernameFromWeb.toUtf8(), m_username.toUtf8()); + const bool passwordEqual = Utils::Password::PBKDF2::verify(m_passwordHash, passwordFromWeb); if (usernameEqual && passwordEqual) { diff --git a/src/webui/api/authcontroller.h b/src/webui/api/authcontroller.h index c44d68753..0a47c2338 100644 --- a/src/webui/api/authcontroller.h +++ b/src/webui/api/authcontroller.h @@ -28,8 +28,10 @@ #pragma once +#include #include #include +#include #include "apicontroller.h" @@ -45,6 +47,9 @@ class AuthController : public APIController public: explicit AuthController(ISessionManager *sessionManager, IApplication *app, QObject *parent = nullptr); + void setUsername(const QString &username); + void setPasswordHash(const QByteArray &passwordHash); + private slots: void loginAction(); void logoutAction() const; @@ -56,6 +61,9 @@ private: ISessionManager *m_sessionManager = nullptr; + QString m_username; + QByteArray m_passwordHash; + struct FailedLogin { int failedAttemptsCount = 0; diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index c85bc9ca8..38a8daa05 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -269,6 +269,16 @@ const Http::Environment &WebApplication::env() const return m_env; } +void WebApplication::setUsername(const QString &username) +{ + m_authController->setUsername(username); +} + +void WebApplication::setPasswordHash(const QByteArray &passwordHash) +{ + m_authController->setPasswordHash(passwordHash); +} + void WebApplication::doProcessRequest() { const QRegularExpressionMatch match = m_apiPathPattern.match(request().path); @@ -357,17 +367,17 @@ void WebApplication::configure() { const auto *pref = Preferences::instance(); - const bool isAltUIUsed = pref->isAltWebUiEnabled(); - const Path rootFolder = (!isAltUIUsed ? Path(WWW_FOLDER) : pref->getWebUiRootFolder()); + const bool isAltUIUsed = pref->isAltWebUIEnabled(); + const Path rootFolder = (!isAltUIUsed ? Path(WWW_FOLDER) : pref->getWebUIRootFolder()); if ((isAltUIUsed != m_isAltUIUsed) || (rootFolder != m_rootFolder)) { m_isAltUIUsed = isAltUIUsed; m_rootFolder = rootFolder; m_translatedFiles.clear(); if (!m_isAltUIUsed) - LogMsg(tr("Using built-in Web UI.")); + LogMsg(tr("Using built-in WebUI.")); else - LogMsg(tr("Using custom Web UI. Location: \"%1\".").arg(m_rootFolder.toString())); + LogMsg(tr("Using custom WebUI. Location: \"%1\".").arg(m_rootFolder.toString())); } const QString newLocale = pref->getLocale(); @@ -379,27 +389,27 @@ void WebApplication::configure() m_translationFileLoaded = m_translator.load((m_rootFolder / Path(u"translations/webui_"_s) + newLocale).data()); if (m_translationFileLoaded) { - LogMsg(tr("Web UI translation for selected locale (%1) has been successfully loaded.") + LogMsg(tr("WebUI translation for selected locale (%1) has been successfully loaded.") .arg(newLocale)); } else { - LogMsg(tr("Couldn't load Web UI translation for selected locale (%1).").arg(newLocale), Log::WARNING); + LogMsg(tr("Couldn't load WebUI translation for selected locale (%1).").arg(newLocale), Log::WARNING); } } - m_isLocalAuthEnabled = pref->isWebUiLocalAuthEnabled(); - m_isAuthSubnetWhitelistEnabled = pref->isWebUiAuthSubnetWhitelistEnabled(); - m_authSubnetWhitelist = pref->getWebUiAuthSubnetWhitelist(); + m_isLocalAuthEnabled = pref->isWebUILocalAuthEnabled(); + m_isAuthSubnetWhitelistEnabled = pref->isWebUIAuthSubnetWhitelistEnabled(); + m_authSubnetWhitelist = pref->getWebUIAuthSubnetWhitelist(); m_sessionTimeout = pref->getWebUISessionTimeout(); m_domainList = pref->getServerDomains().split(u';', Qt::SkipEmptyParts); std::for_each(m_domainList.begin(), m_domainList.end(), [](QString &entry) { entry = entry.trimmed(); }); - m_isCSRFProtectionEnabled = pref->isWebUiCSRFProtectionEnabled(); - m_isSecureCookieEnabled = pref->isWebUiSecureCookieEnabled(); + m_isCSRFProtectionEnabled = pref->isWebUICSRFProtectionEnabled(); + m_isSecureCookieEnabled = pref->isWebUISecureCookieEnabled(); m_isHostHeaderValidationEnabled = pref->isWebUIHostHeaderValidationEnabled(); - m_isHttpsEnabled = pref->isWebUiHttpsEnabled(); + m_isHttpsEnabled = pref->isWebUIHttpsEnabled(); m_prebuiltHeaders.clear(); m_prebuiltHeaders.push_back({Http::HEADER_X_XSS_PROTECTION, u"1; mode=block"_s}); @@ -411,7 +421,7 @@ void WebApplication::configure() m_prebuiltHeaders.push_back({Http::HEADER_REFERRER_POLICY, u"same-origin"_s}); } - const bool isClickjackingProtectionEnabled = pref->isWebUiClickjackingProtectionEnabled(); + const bool isClickjackingProtectionEnabled = pref->isWebUIClickjackingProtectionEnabled(); if (isClickjackingProtectionEnabled) m_prebuiltHeaders.push_back({Http::HEADER_X_FRAME_OPTIONS, u"SAMEORIGIN"_s}); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 929f6e5e6..8d9fbc9fd 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -105,6 +105,9 @@ public: const Http::Request &request() const; const Http::Environment &env() const; + void setUsername(const QString &username); + void setPasswordHash(const QByteArray &passwordHash); + private: void doProcessRequest(); void configure(); diff --git a/src/webui/webui.cpp b/src/webui/webui.cpp index 4b325d785..ab2e9b96e 100644 --- a/src/webui/webui.cpp +++ b/src/webui/webui.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev + * Copyright (C) 2015, 2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -28,6 +28,7 @@ #include "webui.h" +#include "base/global.h" #include "base/http/server.h" #include "base/logger.h" #include "base/net/dnsupdater.h" @@ -36,10 +37,12 @@ #include "base/preferences.h" #include "base/utils/io.h" #include "base/utils/net.h" +#include "base/utils/password.h" #include "webapplication.h" -WebUI::WebUI(IApplication *app) +WebUI::WebUI(IApplication *app, const QByteArray &tempPasswordHash) : ApplicationComponent(app) + , m_passwordHash {tempPasswordHash} { configure(); connect(Preferences::instance(), &Preferences::changed, this, &WebUI::configure); @@ -49,12 +52,23 @@ void WebUI::configure() { m_isErrored = false; // clear previous error state - const QString portForwardingProfile = u"webui"_s; const Preferences *pref = Preferences::instance(); - const quint16 port = pref->getWebUiPort(); + const bool isEnabled = pref->isWebUIEnabled(); + const QString username = pref->getWebUIUsername(); + if (const QByteArray passwordHash = pref->getWebUIPassword(); !passwordHash.isEmpty()) + m_passwordHash = passwordHash; - if (pref->isWebUiEnabled()) + if (isEnabled && (username.isEmpty() || m_passwordHash.isEmpty())) { + setError(tr("Credentials are not set")); + } + + const QString portForwardingProfile = u"webui"_s; + + if (isEnabled && !m_isErrored) + { + const quint16 port = pref->getWebUIPort(); + // Port forwarding auto *portForwarder = Net::PortForwarder::instance(); if (pref->useUPnPForWebUIPort()) @@ -67,7 +81,7 @@ void WebUI::configure() } // http server - const QString serverAddressString = pref->getWebUiAddress(); + const QString serverAddressString = pref->getWebUIAddress(); const auto serverAddress = ((serverAddressString == u"*") || serverAddressString.isEmpty()) ? QHostAddress::Any : QHostAddress(serverAddressString); @@ -82,7 +96,10 @@ void WebUI::configure() m_httpServer->close(); } - if (pref->isWebUiHttpsEnabled()) + m_webapp->setUsername(username); + m_webapp->setPasswordHash(m_passwordHash); + + if (pref->isWebUIHttpsEnabled()) { const auto readData = [](const Path &path) -> QByteArray { @@ -94,9 +111,9 @@ void WebUI::configure() const bool success = m_httpServer->setupHttps(cert, key); if (success) - LogMsg(tr("Web UI: HTTPS setup successful")); + LogMsg(tr("WebUI: HTTPS setup successful")); else - LogMsg(tr("Web UI: HTTPS setup failed, fallback to HTTP"), Log::CRITICAL); + LogMsg(tr("WebUI: HTTPS setup failed, fallback to HTTP"), Log::CRITICAL); } else { @@ -108,17 +125,12 @@ void WebUI::configure() const bool success = m_httpServer->listen(serverAddress, port); if (success) { - LogMsg(tr("Web UI: Now listening on IP: %1, port: %2").arg(serverAddressString).arg(port)); + LogMsg(tr("WebUI: Now listening on IP: %1, port: %2").arg(serverAddressString).arg(port)); } else { - const QString errorMsg = tr("Web UI: Unable to bind to IP: %1, port: %2. Reason: %3") - .arg(serverAddressString).arg(port).arg(m_httpServer->errorString()); - LogMsg(errorMsg, Log::CRITICAL); - qCritical() << errorMsg; - - m_isErrored = true; - emit fatalError(); + setError(tr("Unable to bind to IP: %1, port: %2. Reason: %3") + .arg(serverAddressString).arg(port).arg(m_httpServer->errorString())); } } @@ -145,7 +157,24 @@ void WebUI::configure() } } +void WebUI::setError(const QString &message) +{ + m_isErrored = true; + m_errorMsg = message; + + const QString logMessage = u"WebUI: " + m_errorMsg; + LogMsg(logMessage, Log::CRITICAL); + qCritical() << logMessage; + + emit fatalError(); +} + bool WebUI::isErrored() const { return m_isErrored; } + +QString WebUI::errorMessage() const +{ + return m_errorMsg; +} diff --git a/src/webui/webui.h b/src/webui/webui.h index e2c63b828..cebd39849 100644 --- a/src/webui/webui.h +++ b/src/webui/webui.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev + * Copyright (C) 2015, 2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -51,9 +51,10 @@ class WebUI : public QObject, public ApplicationComponent Q_DISABLE_COPY_MOVE(WebUI) public: - explicit WebUI(IApplication *app); + explicit WebUI(IApplication *app, const QByteArray &tempPasswordHash = {}); bool isErrored() const; + QString errorMessage() const; signals: void fatalError(); @@ -62,8 +63,13 @@ private slots: void configure(); private: + void setError(const QString &message); + bool m_isErrored = false; + QString m_errorMsg; QPointer m_httpServer; QPointer m_dnsUpdater; QPointer m_webapp; + + QByteArray m_passwordHash; }; From 30077628648117a1c033f8fe657d61e374c3a774 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Sat, 11 Nov 2023 16:04:13 +0300 Subject: [PATCH 21/24] WebUI: Correctly set save path in RSS rules PR #19916. Closes #19915. --- src/webui/www/private/views/rssDownloader.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/webui/www/private/views/rssDownloader.html b/src/webui/www/private/views/rssDownloader.html index 73e5bd9af..f75e53280 100644 --- a/src/webui/www/private/views/rssDownloader.html +++ b/src/webui/www/private/views/rssDownloader.html @@ -593,7 +593,10 @@ Supports the formats: S01E01, 1x1, 2017.12.31 and 31.12.2017 (Date formats also rulesList[rule].torrentParams.category = $('assignCategoryCombobox').value; rulesList[rule].torrentParams.tags = $('ruleAddTags').value.split(','); - rulesList[rule].torrentParams.save_path = $('savetoDifferentDir').checked ? $('saveToText').value : ''; + if ($('savetoDifferentDir').checked) { + rulesList[rule].torrentParams.save_path = $('saveToText').value; + rulesList[rule].torrentParams.use_auto_tmm = false; + } switch ($('addPausedCombobox').value) { case 'default': @@ -715,8 +718,6 @@ Supports the formats: S01E01, 1x1, 2017.12.31 and 31.12.2017 (Date formats also $('assignCategoryCombobox').disabled = false; $('ruleAddTags').disabled = false; $('savetoDifferentDir').disabled = false; - $('savetoDifferentDir').checked = rulesList[ruleName].torrentParams.save_path ? false : true; - $('saveToText').disabled = rulesList[ruleName].torrentParams.save_path ? false : true; $('ignoreDaysValue').disabled = false; $('addPausedCombobox').disabled = false; $('contentLayoutCombobox').disabled = false; @@ -731,6 +732,7 @@ Supports the formats: S01E01, 1x1, 2017.12.31 and 31.12.2017 (Date formats also $('assignCategoryCombobox').value = rulesList[ruleName].torrentParams.category ? rulesList[ruleName].torrentParams.category : 'default'; $('ruleAddTags').value = rulesList[ruleName].torrentParams.tags.join(','); $('savetoDifferentDir').checked = rulesList[ruleName].torrentParams.save_path !== ''; + $('saveToText').disabled = !$('savetoDifferentDir').checked; $('saveToText').value = rulesList[ruleName].torrentParams.save_path; $('ignoreDaysValue').value = rulesList[ruleName].ignoreDays; From 43300d97e2f522a49d46796252cb7d76e95db895 Mon Sep 17 00:00:00 2001 From: sledgehammer999 Date: Mon, 20 Mar 2023 17:27:32 +0200 Subject: [PATCH 22/24] WebUI: Minor optimizations to the login page --- src/webui/www/public/index.html | 6 +++--- src/webui/www/public/scripts/login.js | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/webui/www/public/index.html b/src/webui/www/public/index.html index f835e8acc..18c559af7 100644 --- a/src/webui/www/public/index.html +++ b/src/webui/www/public/index.html @@ -23,14 +23,14 @@ qBittorrent logo
-
+

- +

- +
diff --git a/src/webui/www/public/scripts/login.js b/src/webui/www/public/scripts/login.js index 21ccc6ffa..44f40000f 100644 --- a/src/webui/www/public/scripts/login.js +++ b/src/webui/www/public/scripts/login.js @@ -31,13 +31,10 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('username').focus(); document.getElementById('username').select(); - - document.getElementById('loginform').addEventListener('submit', function(e) { - e.preventDefault(); - }); }); -function submitLoginForm() { +function submitLoginForm(event) { + event.preventDefault(); const errorMsgElement = document.getElementById('error_msg'); const xhr = new XMLHttpRequest(); From 59d968e116bbaa60212efeeafad56cc21e338b80 Mon Sep 17 00:00:00 2001 From: Chocobo1 Date: Sun, 12 Nov 2023 17:12:02 +0800 Subject: [PATCH 23/24] Improve performance of getting torrent numbers via WebAPI PR #19919. --- src/webui/api/torrentscontroller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index dfb6a378e..4d20e2727 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -234,7 +234,7 @@ namespace void TorrentsController::countAction() { - setResult(QString::number(BitTorrent::Session::instance()->torrents().count())); + setResult(QString::number(BitTorrent::Session::instance()->torrentsCount())); } // Returns all the torrents in JSON format. From b824889d0753619ab40490b5f869d768f45bc911 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Tue, 7 Nov 2023 12:44:27 +0300 Subject: [PATCH 24/24] Improve free disk space checking for WebAPI Use single free disk space checker instance for all the web sessions. PR #19855. Closes #19732. --- src/webui/CMakeLists.txt | 4 +- src/webui/api/synccontroller.cpp | 43 ++++---------------- src/webui/api/synccontroller.h | 15 ++----- src/webui/{api => }/freediskspacechecker.cpp | 10 ++++- src/webui/{api => }/freediskspacechecker.h | 6 +++ src/webui/webapplication.cpp | 32 +++++++++++++-- src/webui/webapplication.h | 26 ++++++++---- src/webui/webui.pri | 4 +- 8 files changed, 76 insertions(+), 64 deletions(-) rename src/webui/{api => }/freediskspacechecker.cpp (85%) rename src/webui/{api => }/freediskspacechecker.h (92%) diff --git a/src/webui/CMakeLists.txt b/src/webui/CMakeLists.txt index 51cdfe489..610774a72 100644 --- a/src/webui/CMakeLists.txt +++ b/src/webui/CMakeLists.txt @@ -4,7 +4,6 @@ add_library(qbt_webui STATIC api/apierror.h api/appcontroller.h api/authcontroller.h - api/freediskspacechecker.h api/isessionmanager.h api/logcontroller.h api/rsscontroller.h @@ -13,6 +12,7 @@ add_library(qbt_webui STATIC api/torrentscontroller.h api/transfercontroller.h api/serialize/serialize_torrent.h + freediskspacechecker.h webapplication.h webui.h @@ -21,7 +21,6 @@ add_library(qbt_webui STATIC api/apierror.cpp api/appcontroller.cpp api/authcontroller.cpp - api/freediskspacechecker.cpp api/logcontroller.cpp api/rsscontroller.cpp api/searchcontroller.cpp @@ -29,6 +28,7 @@ add_library(qbt_webui STATIC api/torrentscontroller.cpp api/transfercontroller.cpp api/serialize/serialize_torrent.cpp + freediskspacechecker.cpp webapplication.cpp webui.cpp ) diff --git a/src/webui/api/synccontroller.cpp b/src/webui/api/synccontroller.cpp index 830614b28..e484b213e 100644 --- a/src/webui/api/synccontroller.cpp +++ b/src/webui/api/synccontroller.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #include "base/algorithm.h" #include "base/bittorrent/cachestatus.h" @@ -50,13 +49,10 @@ #include "base/preferences.h" #include "base/utils/string.h" #include "apierror.h" -#include "freediskspacechecker.h" #include "serialize/serialize_torrent.h" namespace { - const int FREEDISKSPACE_CHECK_TIMEOUT = 30000; - // Sync main data keys const QString KEY_SYNC_MAINDATA_QUEUEING = u"queueing"_s; const QString KEY_SYNC_MAINDATA_REFRESH_INTERVAL = u"refresh_interval"_s; @@ -391,8 +387,11 @@ namespace SyncController::SyncController(IApplication *app, QObject *parent) : APIController(app, parent) { - invokeChecker(); - m_freeDiskSpaceElapsedTimer.start(); +} + +void SyncController::updateFreeDiskSpace(const qint64 freeDiskSpace) +{ + m_freeDiskSpace = freeDiskSpace; } // The function returns the changed data from the server to synchronize with the web client. @@ -552,7 +551,7 @@ void SyncController::makeMaindataSnapshot() } m_maindataSnapshot.serverState = getTransferInfo(); - m_maindataSnapshot.serverState[KEY_TRANSFER_FREESPACEONDISK] = getFreeDiskSpace(); + m_maindataSnapshot.serverState[KEY_TRANSFER_FREESPACEONDISK] = m_freeDiskSpace; m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled(); m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled(); m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval(); @@ -661,7 +660,7 @@ QJsonObject SyncController::generateMaindataSyncData(const int id, const bool fu m_removedTrackers.clear(); QVariantMap serverState = getTransferInfo(); - serverState[KEY_TRANSFER_FREESPACEONDISK] = getFreeDiskSpace(); + serverState[KEY_TRANSFER_FREESPACEONDISK] = m_freeDiskSpace; serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled(); serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled(); serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval(); @@ -782,34 +781,6 @@ void SyncController::torrentPeersAction() setResult(generateSyncData(acceptedResponseId, data, m_lastAcceptedPeersResponse, m_lastPeersResponse)); } -qint64 SyncController::getFreeDiskSpace() -{ - if (m_freeDiskSpaceElapsedTimer.hasExpired(FREEDISKSPACE_CHECK_TIMEOUT)) - invokeChecker(); - - return m_freeDiskSpace; -} - -void SyncController::invokeChecker() -{ - if (m_isFreeDiskSpaceCheckerRunning) - return; - - auto *freeDiskSpaceChecker = new FreeDiskSpaceChecker; - connect(freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, this, [this](const qint64 freeSpaceSize) - { - m_freeDiskSpace = freeSpaceSize; - m_isFreeDiskSpaceCheckerRunning = false; - m_freeDiskSpaceElapsedTimer.restart(); - }); - connect(freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, freeDiskSpaceChecker, &QObject::deleteLater); - m_isFreeDiskSpaceCheckerRunning = true; - QThreadPool::globalInstance()->start([freeDiskSpaceChecker] - { - freeDiskSpaceChecker->check(); - }); -} - void SyncController::onCategoryAdded(const QString &categoryName) { m_removedCategories.remove(categoryName); diff --git a/src/webui/api/synccontroller.h b/src/webui/api/synccontroller.h index a77487f38..2123f625d 100644 --- a/src/webui/api/synccontroller.h +++ b/src/webui/api/synccontroller.h @@ -28,22 +28,17 @@ #pragma once -#include -#include #include +#include #include "base/bittorrent/infohash.h" #include "apicontroller.h" -class QThread; - namespace BitTorrent { class Torrent; } -class FreeDiskSpaceChecker; - class SyncController : public APIController { Q_OBJECT @@ -54,14 +49,14 @@ public: explicit SyncController(IApplication *app, QObject *parent = nullptr); +public slots: + void updateFreeDiskSpace(qint64 freeDiskSpace); + private slots: void maindataAction(); void torrentPeersAction(); private: - qint64 getFreeDiskSpace(); - void invokeChecker(); - void makeMaindataSnapshot(); QJsonObject generateMaindataSyncData(int id, bool fullUpdate); @@ -85,8 +80,6 @@ private: void onTorrentTrackersChanged(BitTorrent::Torrent *torrent); qint64 m_freeDiskSpace = 0; - QElapsedTimer m_freeDiskSpaceElapsedTimer; - bool m_isFreeDiskSpaceCheckerRunning = false; QVariantMap m_lastPeersResponse; QVariantMap m_lastAcceptedPeersResponse; diff --git a/src/webui/api/freediskspacechecker.cpp b/src/webui/freediskspacechecker.cpp similarity index 85% rename from src/webui/api/freediskspacechecker.cpp rename to src/webui/freediskspacechecker.cpp index 1d1abbe6f..23939fc97 100644 --- a/src/webui/api/freediskspacechecker.cpp +++ b/src/webui/freediskspacechecker.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2018 Thomas Piccirello * * This program is free software; you can redistribute it and/or @@ -31,8 +32,13 @@ #include "base/bittorrent/session.h" #include "base/utils/fs.h" +qint64 FreeDiskSpaceChecker::lastResult() const +{ + return m_lastResult; +} + void FreeDiskSpaceChecker::check() { - const qint64 freeDiskSpace = Utils::Fs::freeDiskSpaceOnPath(BitTorrent::Session::instance()->savePath()); - emit checked(freeDiskSpace); + m_lastResult = Utils::Fs::freeDiskSpaceOnPath(BitTorrent::Session::instance()->savePath()); + emit checked(m_lastResult); } diff --git a/src/webui/api/freediskspacechecker.h b/src/webui/freediskspacechecker.h similarity index 92% rename from src/webui/api/freediskspacechecker.h rename to src/webui/freediskspacechecker.h index a472f35cc..31be38b34 100644 --- a/src/webui/api/freediskspacechecker.h +++ b/src/webui/freediskspacechecker.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2018 Thomas Piccirello * * This program is free software; you can redistribute it and/or @@ -38,9 +39,14 @@ class FreeDiskSpaceChecker final : public QObject public: using QObject::QObject; + qint64 lastResult() const; + public slots: void check(); signals: void checked(qint64 freeSpaceSize); + +private: + qint64 m_lastResult = 0; }; diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index 38a8daa05..be1c19972 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014, 2022 Vladimir Golovnev + * Copyright (C) 2014, 2022-2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -29,16 +29,20 @@ #include "webapplication.h" #include +#include #include #include #include #include #include +#include #include #include #include #include +#include +#include #include #include "base/algorithm.h" @@ -60,6 +64,7 @@ #include "api/synccontroller.h" #include "api/torrentscontroller.h" #include "api/transfercontroller.h" +#include "freediskspacechecker.h" const int MAX_ALLOWED_FILESIZE = 10 * 1024 * 1024; const QString DEFAULT_SESSION_COOKIE_NAME = u"SID"_s; @@ -68,6 +73,10 @@ const QString WWW_FOLDER = u":/www"_s; const QString PUBLIC_FOLDER = u"/public"_s; const QString PRIVATE_FOLDER = u"/private"_s; +using namespace std::chrono_literals; + +const std::chrono::seconds FREEDISKSPACE_CHECK_TIMEOUT = 30s; + namespace { QStringMap parseCookie(const QStringView cookieStr) @@ -147,6 +156,9 @@ WebApplication::WebApplication(IApplication *app, QObject *parent) , ApplicationComponent(app) , m_cacheID {QString::number(Utils::Random::rand(), 36)} , m_authController {new AuthController(this, app, this)} + , m_workerThread {new QThread} + , m_freeDiskSpaceChecker {new FreeDiskSpaceChecker} + , m_freeDiskSpaceCheckingTimer {new QTimer(this)} { declarePublicAPI(u"auth/login"_s); @@ -163,6 +175,16 @@ WebApplication::WebApplication(IApplication *app, QObject *parent) } m_sessionCookieName = DEFAULT_SESSION_COOKIE_NAME; } + + m_freeDiskSpaceChecker->moveToThread(m_workerThread.get()); + connect(m_workerThread.get(), &QThread::finished, m_freeDiskSpaceChecker, &QObject::deleteLater); + m_workerThread->start(); + + m_freeDiskSpaceCheckingTimer->setInterval(FREEDISKSPACE_CHECK_TIMEOUT); + m_freeDiskSpaceCheckingTimer->setSingleShot(true); + connect(m_freeDiskSpaceCheckingTimer, &QTimer::timeout, m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::check); + connect(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, m_freeDiskSpaceCheckingTimer, qOverload<>(&QTimer::start)); + QMetaObject::invokeMethod(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::check); } WebApplication::~WebApplication() @@ -690,14 +712,18 @@ void WebApplication::sessionStart() }); m_currentSession = new WebSession(generateSid(), app()); + m_sessions[m_currentSession->id()] = m_currentSession; + m_currentSession->registerAPIController(u"app"_s); m_currentSession->registerAPIController(u"log"_s); m_currentSession->registerAPIController(u"rss"_s); m_currentSession->registerAPIController(u"search"_s); - m_currentSession->registerAPIController(u"sync"_s); m_currentSession->registerAPIController(u"torrents"_s); m_currentSession->registerAPIController(u"transfer"_s); - m_sessions[m_currentSession->id()] = m_currentSession; + + auto *syncController = m_currentSession->registerAPIController(u"sync"_s); + syncController->updateFreeDiskSpace(m_freeDiskSpaceChecker->lastResult()); + connect(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, syncController, &SyncController::updateFreeDiskSpace); QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toUtf8()}; cookie.setHttpOnly(true); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 8d9fbc9fd..95313583a 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014, 2017, 2022 Vladimir Golovnev + * Copyright (C) 2014, 2017, 2022-2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -49,13 +49,17 @@ #include "base/http/types.h" #include "base/path.h" #include "base/utils/net.h" +#include "base/utils/thread.h" #include "base/utils/version.h" #include "api/isessionmanager.h" inline const Utils::Version<3, 2> API_VERSION {2, 9, 3}; +class QTimer; + class APIController; class AuthController; +class FreeDiskSpaceChecker; class WebApplication; class WebSession final : public QObject, public ApplicationComponent, public ISession @@ -69,10 +73,12 @@ public: void updateTimestamp(); template - void registerAPIController(const QString &scope) + T *registerAPIController(const QString &scope) { static_assert(std::is_base_of_v, "Class should be derived from APIController."); - m_apiControllers[scope] = new T(app(), this); + auto *controller = new T(app(), this); + m_apiControllers[scope] = controller; + return controller; } APIController *getAPIController(const QString &scope) const; @@ -97,11 +103,6 @@ public: Http::Response processRequest(const Http::Request &request, const Http::Environment &env) override; - QString clientId() const override; - WebSession *session() override; - void sessionStart() override; - void sessionEnd() override; - const Http::Request &request() const; const Http::Environment &env() const; @@ -109,6 +110,11 @@ public: void setPasswordHash(const QByteArray &passwordHash); private: + QString clientId() const override; + WebSession *session() override; + void sessionStart() override; + void sessionEnd() override; + void doProcessRequest(); void configure(); @@ -244,4 +250,8 @@ private: QHostAddress m_clientAddress; QVector m_prebuiltHeaders; + + Utils::Thread::UniquePtr m_workerThread; + FreeDiskSpaceChecker *m_freeDiskSpaceChecker = nullptr; + QTimer *m_freeDiskSpaceCheckingTimer = nullptr; }; diff --git a/src/webui/webui.pri b/src/webui/webui.pri index 1f1135d1a..8d7213569 100644 --- a/src/webui/webui.pri +++ b/src/webui/webui.pri @@ -3,7 +3,6 @@ HEADERS += \ $$PWD/api/apierror.h \ $$PWD/api/appcontroller.h \ $$PWD/api/authcontroller.h \ - $$PWD/api/freediskspacechecker.h \ $$PWD/api/isessionmanager.h \ $$PWD/api/logcontroller.h \ $$PWD/api/rsscontroller.h \ @@ -12,6 +11,7 @@ HEADERS += \ $$PWD/api/torrentscontroller.h \ $$PWD/api/transfercontroller.h \ $$PWD/api/serialize/serialize_torrent.h \ + $$PWD/freediskspacechecker.h \ $$PWD/webapplication.h \ $$PWD/webui.h @@ -20,7 +20,6 @@ SOURCES += \ $$PWD/api/apierror.cpp \ $$PWD/api/appcontroller.cpp \ $$PWD/api/authcontroller.cpp \ - $$PWD/api/freediskspacechecker.cpp \ $$PWD/api/logcontroller.cpp \ $$PWD/api/rsscontroller.cpp \ $$PWD/api/searchcontroller.cpp \ @@ -28,6 +27,7 @@ SOURCES += \ $$PWD/api/torrentscontroller.cpp \ $$PWD/api/transfercontroller.cpp \ $$PWD/api/serialize/serialize_torrent.cpp \ + $$PWD/freediskspacechecker.cpp \ $$PWD/webapplication.cpp \ $$PWD/webui.cpp