Backport changes to v4.6.x branch

PR #19751.
This commit is contained in:
Vladimir Golovnev 2023-11-13 14:29:43 +03:00 committed by GitHub
commit 267d504ec0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 717 additions and 397 deletions

View file

@ -90,34 +90,35 @@ jobs:
--recurse-submodules ` --recurse-submodules `
https://github.com/arvidn/libtorrent.git https://github.com/arvidn/libtorrent.git
cd libtorrent cd libtorrent
$env:CXXFLAGS+=" /guard:cf"
$env:LDFLAGS+=" /guard:cf"
cmake ` cmake `
-B build ` -B build `
-G "Ninja" ` -G "Ninja" `
-DCMAKE_BUILD_TYPE=RelWithDebInfo ` -DCMAKE_BUILD_TYPE=RelWithDebInfo `
-DCMAKE_CXX_FLAGS=/guard:cf `
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON ` -DCMAKE_EXPORT_COMPILE_COMMANDS=ON `
-DCMAKE_INSTALL_PREFIX="${{ env.libtorrent_path }}" ` -DCMAKE_INSTALL_PREFIX="${{ env.libtorrent_path }}" `
-DCMAKE_TOOLCHAIN_FILE="${{ env.RUNVCPKG_VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" ` -DCMAKE_TOOLCHAIN_FILE="${{ env.RUNVCPKG_VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" `
-DBOOST_ROOT="${{ env.boost_path }}" ` -DBOOST_ROOT="${{ env.boost_path }}" `
-DBUILD_SHARED_LIBS=OFF ` -DBUILD_SHARED_LIBS=OFF `
-Ddeprecated-functions=OFF ` -Ddeprecated-functions=OFF `
-Dstatic_runtime=ON ` -Dstatic_runtime=OFF `
-DVCPKG_TARGET_TRIPLET=x64-windows-static-release -DVCPKG_TARGET_TRIPLET=x64-windows-static-release
cmake --build build cmake --build build
cmake --install build cmake --install build
- name: Build qBittorrent - name: Build qBittorrent
run: | run: |
$env:CXXFLAGS+=" /WX"
cmake ` cmake `
-B build ` -B build `
-G "Ninja" ` -G "Ninja" `
-DCMAKE_BUILD_TYPE=RelWithDebInfo ` -DCMAKE_BUILD_TYPE=RelWithDebInfo `
-DCMAKE_CXX_FLAGS="/WX" `
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON ` -DCMAKE_EXPORT_COMPILE_COMMANDS=ON `
-DCMAKE_TOOLCHAIN_FILE="${{ env.RUNVCPKG_VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" ` -DCMAKE_TOOLCHAIN_FILE="${{ env.RUNVCPKG_VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" `
-DBOOST_ROOT="${{ env.boost_path }}" ` -DBOOST_ROOT="${{ env.boost_path }}" `
-DLibtorrentRasterbar_DIR="${{ env.libtorrent_path }}/lib/cmake/LibtorrentRasterbar" ` -DLibtorrentRasterbar_DIR="${{ env.libtorrent_path }}/lib/cmake/LibtorrentRasterbar" `
-DMSVC_RUNTIME_DYNAMIC=OFF ` -DMSVC_RUNTIME_DYNAMIC=ON `
-DQT6=ON ` -DQT6=ON `
-DTESTING=ON ` -DTESTING=ON `
-DVCPKG_TARGET_TRIPLET=x64-windows-static-release ` -DVCPKG_TARGET_TRIPLET=x64-windows-static-release `

View file

@ -96,7 +96,6 @@
#include "gui/mainwindow.h" #include "gui/mainwindow.h"
#include "gui/shutdownconfirmdialog.h" #include "gui/shutdownconfirmdialog.h"
#include "gui/uithememanager.h" #include "gui/uithememanager.h"
#include "gui/utils.h"
#include "gui/windowstate.h" #include "gui/windowstate.h"
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
@ -106,6 +105,9 @@
#ifndef DISABLE_WEBUI #ifndef DISABLE_WEBUI
#include "webui/webui.h" #include "webui/webui.h"
#ifdef DISABLE_GUI
#include "base/utils/password.h"
#endif
#endif #endif
namespace namespace
@ -310,8 +312,8 @@ Application::Application(int &argc, char **argv)
if (isFileLoggerEnabled()) if (isFileLoggerEnabled())
m_fileLogger = new FileLogger(fileLoggerPath(), isFileLoggerBackup(), fileLoggerMaxSize(), isFileLoggerDeleteOld(), fileLoggerAge(), static_cast<FileLogger::FileLogAgeType>(fileLoggerAgeType())); m_fileLogger = new FileLogger(fileLoggerPath(), isFileLoggerBackup(), fileLoggerMaxSize(), isFileLoggerDeleteOld(), fileLoggerAge(), static_cast<FileLogger::FileLogAgeType>(fileLoggerAgeType()));
if (m_commandLineArgs.webUiPort > 0) // it will be -1 when user did not set any value if (m_commandLineArgs.webUIPort > 0) // it will be -1 when user did not set any value
Preferences::instance()->setWebUiPort(m_commandLineArgs.webUiPort); Preferences::instance()->setWebUIPort(m_commandLineArgs.webUIPort);
if (m_commandLineArgs.torrentingPort > 0) // it will be -1 when user did not set any value if (m_commandLineArgs.torrentingPort > 0) // it will be -1 when user did not set any value
{ {
@ -375,7 +377,7 @@ void Application::setMemoryWorkingSetLimit(const int size)
return; return;
m_storeMemoryWorkingSetLimit = size; m_storeMemoryWorkingSetLimit = size;
#ifdef QBT_USES_LIBTORRENT2 #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
applyMemoryWorkingSetLimit(); applyMemoryWorkingSetLimit();
#endif #endif
} }
@ -773,7 +775,7 @@ int Application::exec()
printf("%s\n", qUtf8Printable(loadingStr)); printf("%s\n", qUtf8Printable(loadingStr));
#endif #endif
#ifdef QBT_USES_LIBTORRENT2 #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
applyMemoryWorkingSetLimit(); applyMemoryWorkingSetLimit();
#endif #endif
@ -899,25 +901,28 @@ int Application::exec()
#endif // DISABLE_GUI #endif // DISABLE_GUI
#ifndef DISABLE_WEBUI #ifndef DISABLE_WEBUI
#ifndef DISABLE_GUI
m_webui = new WebUI(this); 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()) if (m_webui->isErrored())
QCoreApplication::exit(EXIT_FAILURE); QCoreApplication::exit(EXIT_FAILURE);
connect(m_webui, &WebUI::fatalError, this, []() { 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")) const QString mesg = u"\n******** %1 ********\n"_s.arg(tr("Information"))
+ tr("To control qBittorrent, access the WebUI at: %1").arg(url); + tr("To control qBittorrent, access the WebUI at: %1").arg(url);
printf("%s\n", qUtf8Printable(mesg)); 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' const QString warning = tr("The WebUI 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("The WebUI administrator password was not set. A temporary password is provided for this session: %1").arg(tempPassword) + u'\n'
+ tr("This is a security risk, please change your password in program preferences.") + u'\n'; + tr("You should set your own password in program preferences.") + u'\n';
printf("%s", qUtf8Printable(warning)); printf("%s", qUtf8Printable(warning));
} }
#endif // DISABLE_GUI #endif // DISABLE_GUI
@ -1080,7 +1085,7 @@ void Application::shutdownCleanup(QSessionManager &manager)
} }
#endif #endif
#ifdef QBT_USES_LIBTORRENT2 #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
void Application::applyMemoryWorkingSetLimit() const void Application::applyMemoryWorkingSetLimit() const
{ {
const size_t MiB = 1024 * 1024; const size_t MiB = 1024 * 1024;
@ -1300,3 +1305,10 @@ void Application::cleanup()
Utils::Misc::shutdownComputer(m_shutdownAct); Utils::Misc::shutdownComputer(m_shutdownAct);
} }
} }
#ifndef DISABLE_WEBUI
WebUI *Application::webUI() const
{
return m_webui;
}
#endif

View file

@ -149,12 +149,16 @@ private slots:
#endif #endif
private: private:
#ifndef DISABLE_WEBUI
WebUI *webUI() const override;
#endif
void initializeTranslation(); void initializeTranslation();
void processParams(const QBtCommandLineParameters &params); void processParams(const QBtCommandLineParameters &params);
void runExternalProgram(const QString &programTemplate, const BitTorrent::Torrent *torrent) const; void runExternalProgram(const QString &programTemplate, const BitTorrent::Torrent *torrent) const;
void sendNotificationEmail(const BitTorrent::Torrent *torrent); void sendNotificationEmail(const BitTorrent::Torrent *torrent);
#ifdef QBT_USES_LIBTORRENT2 #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
void applyMemoryWorkingSetLimit() const; void applyMemoryWorkingSetLimit() const;
#endif #endif

View file

@ -349,7 +349,7 @@ QBtCommandLineParameters::QBtCommandLineParameters(const QProcessEnvironment &en
#elif !defined(Q_OS_WIN) #elif !defined(Q_OS_WIN)
, shouldDaemonize(DAEMON_OPTION.value(env)) , shouldDaemonize(DAEMON_OPTION.value(env))
#endif #endif
, webUiPort(WEBUI_PORT_OPTION.value(env, -1)) , webUIPort(WEBUI_PORT_OPTION.value(env, -1))
, torrentingPort(TORRENTING_PORT_OPTION.value(env, -1)) , torrentingPort(TORRENTING_PORT_OPTION.value(env, -1))
, skipDialog(SKIP_DIALOG_OPTION.value(env)) , skipDialog(SKIP_DIALOG_OPTION.value(env))
, profileDir(PROFILE_OPTION.value(env)) , profileDir(PROFILE_OPTION.value(env))
@ -387,8 +387,8 @@ QBtCommandLineParameters parseCommandLine(const QStringList &args)
#endif #endif
else if (arg == WEBUI_PORT_OPTION) else if (arg == WEBUI_PORT_OPTION)
{ {
result.webUiPort = WEBUI_PORT_OPTION.value(arg); result.webUIPort = WEBUI_PORT_OPTION.value(arg);
if ((result.webUiPort < 1) || (result.webUiPort > 65535)) if ((result.webUIPort < 1) || (result.webUIPort > 65535))
throw CommandLineParameterError(QCoreApplication::translate("CMD Options", "%1 must specify a valid port (1 to 65535).") throw CommandLineParameterError(QCoreApplication::translate("CMD Options", "%1 must specify a valid port (1 to 65535).")
.arg(u"--webui-port"_s)); .arg(u"--webui-port"_s));
} }

View file

@ -53,7 +53,7 @@ struct QBtCommandLineParameters
#elif !defined(Q_OS_WIN) #elif !defined(Q_OS_WIN)
bool shouldDaemonize = false; bool shouldDaemonize = false;
#endif #endif
int webUiPort = -1; int webUIPort = -1;
int torrentingPort = -1; int torrentingPort = -1;
std::optional<bool> skipDialog; std::optional<bool> skipDialog;
Path profileDir; Path profileDir;

View file

@ -2435,6 +2435,11 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id)
return false; return false;
const lt::torrent_handle nativeHandle = downloadedMetadataIter.value(); const lt::torrent_handle nativeHandle = downloadedMetadataIter.value();
m_downloadedMetadata.erase(downloadedMetadataIter);
if (!nativeHandle.is_valid())
return true;
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
const InfoHash infoHash {nativeHandle.info_hashes()}; const InfoHash infoHash {nativeHandle.info_hashes()};
if (infoHash.isHybrid()) if (infoHash.isHybrid())
@ -2445,7 +2450,7 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id)
m_downloadedMetadata.remove((altID == downloadedMetadataIter.key()) ? id : altID); m_downloadedMetadata.remove((altID == downloadedMetadataIter.key()) ? id : altID);
} }
#endif #endif
m_downloadedMetadata.erase(downloadedMetadataIter);
m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_files); m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_files);
return true; return true;
} }

View file

@ -1811,6 +1811,7 @@ void TorrentImpl::moveStorage(const Path &newPath, const MoveStorageContext cont
{ {
if (!hasMetadata()) if (!hasMetadata())
{ {
m_savePath = newPath;
m_session->handleTorrentSavePathChanged(this); m_session->handleTorrentSavePathChanged(this);
return; return;
} }

View file

@ -36,6 +36,7 @@
class QString; class QString;
class Path; class Path;
class WebUI;
struct QBtCommandLineParameters; struct QBtCommandLineParameters;
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
@ -83,4 +84,8 @@ public:
virtual MemoryPriority processMemoryPriority() const = 0; virtual MemoryPriority processMemoryPriority() const = 0;
virtual void setProcessMemoryPriority(MemoryPriority priority) = 0; virtual void setProcessMemoryPriority(MemoryPriority priority) = 0;
#endif #endif
#ifndef DISABLE_WEBUI
virtual WebUI *webUI() const = 0;
#endif
}; };

View file

@ -628,7 +628,7 @@ void Preferences::setSearchEnabled(const bool enabled)
setValue(u"Preferences/Search/SearchEnabled"_s, enabled); setValue(u"Preferences/Search/SearchEnabled"_s, enabled);
} }
bool Preferences::isWebUiEnabled() const bool Preferences::isWebUIEnabled() const
{ {
#ifdef DISABLE_GUI #ifdef DISABLE_GUI
const bool defaultValue = true; const bool defaultValue = true;
@ -638,41 +638,41 @@ bool Preferences::isWebUiEnabled() const
return value(u"Preferences/WebUI/Enabled"_s, defaultValue); 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; return;
setValue(u"Preferences/WebUI/Enabled"_s, enabled); setValue(u"Preferences/WebUI/Enabled"_s, enabled);
} }
bool Preferences::isWebUiLocalAuthEnabled() const bool Preferences::isWebUILocalAuthEnabled() const
{ {
return value(u"Preferences/WebUI/LocalHostAuth"_s, true); 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; return;
setValue(u"Preferences/WebUI/LocalHostAuth"_s, enabled); setValue(u"Preferences/WebUI/LocalHostAuth"_s, enabled);
} }
bool Preferences::isWebUiAuthSubnetWhitelistEnabled() const bool Preferences::isWebUIAuthSubnetWhitelistEnabled() const
{ {
return value(u"Preferences/WebUI/AuthSubnetWhitelistEnabled"_s, false); 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; return;
setValue(u"Preferences/WebUI/AuthSubnetWhitelistEnabled"_s, enabled); setValue(u"Preferences/WebUI/AuthSubnetWhitelistEnabled"_s, enabled);
} }
QVector<Utils::Net::Subnet> Preferences::getWebUiAuthSubnetWhitelist() const QVector<Utils::Net::Subnet> Preferences::getWebUIAuthSubnetWhitelist() const
{ {
const auto subnets = value<QStringList>(u"Preferences/WebUI/AuthSubnetWhitelist"_s); const auto subnets = value<QStringList>(u"Preferences/WebUI/AuthSubnetWhitelist"_s);
@ -689,7 +689,7 @@ QVector<Utils::Net::Subnet> Preferences::getWebUiAuthSubnetWhitelist() const
return ret; return ret;
} }
void Preferences::setWebUiAuthSubnetWhitelist(QStringList subnets) void Preferences::setWebUIAuthSubnetWhitelist(QStringList subnets)
{ {
Algorithm::removeIf(subnets, [](const QString &subnet) Algorithm::removeIf(subnets, [](const QString &subnet)
{ {
@ -712,27 +712,27 @@ void Preferences::setServerDomains(const QString &str)
setValue(u"Preferences/WebUI/ServerDomains"_s, str); setValue(u"Preferences/WebUI/ServerDomains"_s, str);
} }
QString Preferences::getWebUiAddress() const QString Preferences::getWebUIAddress() const
{ {
return value<QString>(u"Preferences/WebUI/Address"_s, u"*"_s).trimmed(); return value<QString>(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; return;
setValue(u"Preferences/WebUI/Address"_s, addr.trimmed()); setValue(u"Preferences/WebUI/Address"_s, addr.trimmed());
} }
quint16 Preferences::getWebUiPort() const quint16 Preferences::getWebUIPort() const
{ {
return value<quint16>(u"Preferences/WebUI/Port"_s, 8080); return value<quint16>(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; return;
// cast to `int` type so it will show human readable unit in configuration file // 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); setValue(u"Preferences/WebUI/UseUPnP"_s, enabled);
} }
QString Preferences::getWebUiUsername() const QString Preferences::getWebUIUsername() const
{ {
return value<QString>(u"Preferences/WebUI/Username"_s, u"admin"_s); return value<QString>(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; return;
setValue(u"Preferences/WebUI/Username"_s, username); setValue(u"Preferences/WebUI/Username"_s, username);
@ -767,9 +767,7 @@ void Preferences::setWebUiUsername(const QString &username)
QByteArray Preferences::getWebUIPassword() const QByteArray Preferences::getWebUIPassword() const
{ {
// default: adminadmin return value<QByteArray>(u"Preferences/WebUI/Password_PBKDF2"_s);
const auto defaultValue = QByteArrayLiteral("ARQ77eY1NUZaQsuDHbIMCA==:0WMRkYTUWVT9wVvdDtHAjU9b3b7uB8NR1Gur2hmQCvCDpm39Q+PsJRJPaCU51dEiz+dTzh8qbPsL8WkFljQYFQ==");
return value(u"Preferences/WebUI/Password_PBKDF2"_s, defaultValue);
} }
void Preferences::setWebUIPassword(const QByteArray &password) void Preferences::setWebUIPassword(const QByteArray &password)
@ -832,40 +830,40 @@ void Preferences::setWebAPISessionCookieName(const QString &cookieName)
setValue(u"WebAPI/SessionCookieName"_s, cookieName); setValue(u"WebAPI/SessionCookieName"_s, cookieName);
} }
bool Preferences::isWebUiClickjackingProtectionEnabled() const bool Preferences::isWebUIClickjackingProtectionEnabled() const
{ {
return value(u"Preferences/WebUI/ClickjackingProtection"_s, true); 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; return;
setValue(u"Preferences/WebUI/ClickjackingProtection"_s, enabled); setValue(u"Preferences/WebUI/ClickjackingProtection"_s, enabled);
} }
bool Preferences::isWebUiCSRFProtectionEnabled() const bool Preferences::isWebUICSRFProtectionEnabled() const
{ {
return value(u"Preferences/WebUI/CSRFProtection"_s, true); 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; return;
setValue(u"Preferences/WebUI/CSRFProtection"_s, enabled); setValue(u"Preferences/WebUI/CSRFProtection"_s, enabled);
} }
bool Preferences::isWebUiSecureCookieEnabled() const bool Preferences::isWebUISecureCookieEnabled() const
{ {
return value(u"Preferences/WebUI/SecureCookie"_s, true); 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; return;
setValue(u"Preferences/WebUI/SecureCookie"_s, enabled); setValue(u"Preferences/WebUI/SecureCookie"_s, enabled);
@ -884,14 +882,14 @@ void Preferences::setWebUIHostHeaderValidationEnabled(const bool enabled)
setValue(u"Preferences/WebUI/HostHeaderValidation"_s, 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); 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; return;
setValue(u"Preferences/WebUI/HTTPS/Enabled"_s, enabled); 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); setValue(u"Preferences/WebUI/HTTPS/KeyPath"_s, path);
} }
bool Preferences::isAltWebUiEnabled() const bool Preferences::isAltWebUIEnabled() const
{ {
return value(u"Preferences/WebUI/AlternativeUIEnabled"_s, false); 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; return;
setValue(u"Preferences/WebUI/AlternativeUIEnabled"_s, enabled); setValue(u"Preferences/WebUI/AlternativeUIEnabled"_s, enabled);
} }
Path Preferences::getWebUiRootFolder() const Path Preferences::getWebUIRootFolder() const
{ {
return value<Path>(u"Preferences/WebUI/RootFolder"_s); return value<Path>(u"Preferences/WebUI/RootFolder"_s);
} }
void Preferences::setWebUiRootFolder(const Path &path) void Preferences::setWebUIRootFolder(const Path &path)
{ {
if (path == getWebUiRootFolder()) if (path == getWebUIRootFolder())
return; return;
setValue(u"Preferences/WebUI/RootFolder"_s, path); setValue(u"Preferences/WebUI/RootFolder"_s, path);

View file

@ -169,26 +169,26 @@ public:
void setSearchEnabled(bool enabled); void setSearchEnabled(bool enabled);
// HTTP Server // HTTP Server
bool isWebUiEnabled() const; bool isWebUIEnabled() const;
void setWebUiEnabled(bool enabled); void setWebUIEnabled(bool enabled);
QString getServerDomains() const; QString getServerDomains() const;
void setServerDomains(const QString &str); void setServerDomains(const QString &str);
QString getWebUiAddress() const; QString getWebUIAddress() const;
void setWebUiAddress(const QString &addr); void setWebUIAddress(const QString &addr);
quint16 getWebUiPort() const; quint16 getWebUIPort() const;
void setWebUiPort(quint16 port); void setWebUIPort(quint16 port);
bool useUPnPForWebUIPort() const; bool useUPnPForWebUIPort() const;
void setUPnPForWebUIPort(bool enabled); void setUPnPForWebUIPort(bool enabled);
// Authentication // Authentication
bool isWebUiLocalAuthEnabled() const; bool isWebUILocalAuthEnabled() const;
void setWebUiLocalAuthEnabled(bool enabled); void setWebUILocalAuthEnabled(bool enabled);
bool isWebUiAuthSubnetWhitelistEnabled() const; bool isWebUIAuthSubnetWhitelistEnabled() const;
void setWebUiAuthSubnetWhitelistEnabled(bool enabled); void setWebUIAuthSubnetWhitelistEnabled(bool enabled);
QVector<Utils::Net::Subnet> getWebUiAuthSubnetWhitelist() const; QVector<Utils::Net::Subnet> getWebUIAuthSubnetWhitelist() const;
void setWebUiAuthSubnetWhitelist(QStringList subnets); void setWebUIAuthSubnetWhitelist(QStringList subnets);
QString getWebUiUsername() const; QString getWebUIUsername() const;
void setWebUiUsername(const QString &username); void setWebUIUsername(const QString &username);
QByteArray getWebUIPassword() const; QByteArray getWebUIPassword() const;
void setWebUIPassword(const QByteArray &password); void setWebUIPassword(const QByteArray &password);
int getWebUIMaxAuthFailCount() const; int getWebUIMaxAuthFailCount() const;
@ -201,26 +201,26 @@ public:
void setWebAPISessionCookieName(const QString &cookieName); void setWebAPISessionCookieName(const QString &cookieName);
// WebUI security // WebUI security
bool isWebUiClickjackingProtectionEnabled() const; bool isWebUIClickjackingProtectionEnabled() const;
void setWebUiClickjackingProtectionEnabled(bool enabled); void setWebUIClickjackingProtectionEnabled(bool enabled);
bool isWebUiCSRFProtectionEnabled() const; bool isWebUICSRFProtectionEnabled() const;
void setWebUiCSRFProtectionEnabled(bool enabled); void setWebUICSRFProtectionEnabled(bool enabled);
bool isWebUiSecureCookieEnabled () const; bool isWebUISecureCookieEnabled () const;
void setWebUiSecureCookieEnabled(bool enabled); void setWebUISecureCookieEnabled(bool enabled);
bool isWebUIHostHeaderValidationEnabled() const; bool isWebUIHostHeaderValidationEnabled() const;
void setWebUIHostHeaderValidationEnabled(bool enabled); void setWebUIHostHeaderValidationEnabled(bool enabled);
// HTTPS // HTTPS
bool isWebUiHttpsEnabled() const; bool isWebUIHttpsEnabled() const;
void setWebUiHttpsEnabled(bool enabled); void setWebUIHttpsEnabled(bool enabled);
Path getWebUIHttpsCertificatePath() const; Path getWebUIHttpsCertificatePath() const;
void setWebUIHttpsCertificatePath(const Path &path); void setWebUIHttpsCertificatePath(const Path &path);
Path getWebUIHttpsKeyPath() const; Path getWebUIHttpsKeyPath() const;
void setWebUIHttpsKeyPath(const Path &path); void setWebUIHttpsKeyPath(const Path &path);
bool isAltWebUiEnabled() const; bool isAltWebUIEnabled() const;
void setAltWebUiEnabled(bool enabled); void setAltWebUIEnabled(bool enabled);
Path getWebUiRootFolder() const; Path getWebUIRootFolder() const;
void setWebUiRootFolder(const Path &path); void setWebUIRootFolder(const Path &path);
// WebUI custom HTTP headers // WebUI custom HTTP headers
bool isWebUICustomHTTPHeadersEnabled() const; bool isWebUICustomHTTPHeadersEnabled() const;

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Mike Tzou (Chocobo1) * Copyright (C) 2018 Mike Tzou (Chocobo1)
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -36,6 +37,7 @@
#include <QString> #include <QString>
#include <QVector> #include <QVector>
#include "base/global.h"
#include "bytearray.h" #include "bytearray.h"
#include "random.h" #include "random.h"
@ -65,6 +67,21 @@ bool Utils::Password::slowEquals(const QByteArray &a, const QByteArray &b)
return (diff == 0); 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) QByteArray Utils::Password::PBKDF2::generate(const QString &password)
{ {
return generate(password.toUtf8()); return generate(password.toUtf8());
@ -72,9 +89,8 @@ QByteArray Utils::Password::PBKDF2::generate(const QString &password)
QByteArray Utils::Password::PBKDF2::generate(const QByteArray &password) QByteArray Utils::Password::PBKDF2::generate(const QByteArray &password)
{ {
const std::array<uint32_t, 4> salt const std::array<uint32_t, 4> salt {
{{Random::rand(), Random::rand() {Random::rand(), Random::rand(), Random::rand(), Random::rand()}};
, Random::rand(), Random::rand()}};
std::array<unsigned char, 64> outBuf {}; std::array<unsigned char, 64> outBuf {};
const int hmacResult = PKCS5_PBKDF2_HMAC(password.constData(), password.size() const int hmacResult = PKCS5_PBKDF2_HMAC(password.constData(), password.size()

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Mike Tzou (Chocobo1) * Copyright (C) 2018 Mike Tzou (Chocobo1)
* *
* This program is free software; you can redistribute it and/or * 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 // Taken from https://crackstation.net/hashing-security.htm
bool slowEquals(const QByteArray &a, const QByteArray &b); bool slowEquals(const QByteArray &a, const QByteArray &b);
QString generate();
namespace PBKDF2 namespace PBKDF2
{ {
QByteArray generate(const QString &password); QByteArray generate(const QString &password);

View file

@ -30,6 +30,7 @@
#include "addnewtorrentdialog.h" #include "addnewtorrentdialog.h"
#include <algorithm> #include <algorithm>
#include <functional>
#include <QAction> #include <QAction>
#include <QDateTime> #include <QDateTime>
@ -141,10 +142,11 @@ class AddNewTorrentDialog::TorrentContentAdaptor final
{ {
public: public:
TorrentContentAdaptor(BitTorrent::TorrentInfo &torrentInfo, PathList &filePaths TorrentContentAdaptor(BitTorrent::TorrentInfo &torrentInfo, PathList &filePaths
, QVector<BitTorrent::DownloadPriority> &filePriorities) , QVector<BitTorrent::DownloadPriority> &filePriorities, std::function<void ()> onFilePrioritiesChanged)
: m_torrentInfo {torrentInfo} : m_torrentInfo {torrentInfo}
, m_filePaths {filePaths} , m_filePaths {filePaths}
, m_filePriorities {filePriorities} , m_filePriorities {filePriorities}
, m_onFilePrioritiesChanged {std::move(onFilePrioritiesChanged)}
{ {
Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount())); Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount()));
@ -255,6 +257,8 @@ public:
{ {
Q_ASSERT(priorities.size() == filesCount()); Q_ASSERT(priorities.size() == filesCount());
m_filePriorities = priorities; m_filePriorities = priorities;
if (m_onFilePrioritiesChanged)
m_onFilePrioritiesChanged();
} }
Path actualStorageLocation() const override Path actualStorageLocation() const override
@ -275,6 +279,7 @@ private:
BitTorrent::TorrentInfo &m_torrentInfo; BitTorrent::TorrentInfo &m_torrentInfo;
PathList &m_filePaths; PathList &m_filePaths;
QVector<BitTorrent::DownloadPriority> &m_filePriorities; QVector<BitTorrent::DownloadPriority> &m_filePriorities;
std::function<void ()> m_onFilePrioritiesChanged;
Path m_originalRootFolder; Path m_originalRootFolder;
BitTorrent::TorrentContentLayout m_currentContentLayout; BitTorrent::TorrentContentLayout m_currentContentLayout;
}; };
@ -767,7 +772,7 @@ void AddNewTorrentDialog::contentLayoutChanged()
const auto contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex()); const auto contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex());
m_contentAdaptor->applyContentLayout(contentLayout); 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() void AddNewTorrentDialog::saveTorrentFile()
@ -992,7 +997,8 @@ void AddNewTorrentDialog::setupTreeview()
if (m_torrentParams.filePaths.isEmpty()) if (m_torrentParams.filePaths.isEmpty())
m_torrentParams.filePaths = m_torrentInfo.filePaths(); m_torrentParams.filePaths = m_torrentInfo.filePaths();
m_contentAdaptor = new TorrentContentAdaptor(m_torrentInfo, m_torrentParams.filePaths, m_torrentParams.filePriorities); m_contentAdaptor = std::make_unique<TorrentContentAdaptor>(m_torrentInfo, m_torrentParams.filePaths
, m_torrentParams.filePriorities, [this] { updateDiskSpaceLabel(); });
const auto contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex()); const auto contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex());
m_contentAdaptor->applyContentLayout(contentLayout); m_contentAdaptor->applyContentLayout(contentLayout);
@ -1013,7 +1019,7 @@ void AddNewTorrentDialog::setupTreeview()
m_contentAdaptor->prioritizeFiles(priorities); m_contentAdaptor->prioritizeFiles(priorities);
} }
m_ui->contentTreeView->setContentHandler(m_contentAdaptor); m_ui->contentTreeView->setContentHandler(m_contentAdaptor.get());
m_filterLine->blockSignals(false); m_filterLine->blockSignals(false);

View file

@ -116,7 +116,7 @@ private:
void showEvent(QShowEvent *event) override; void showEvent(QShowEvent *event) override;
Ui::AddNewTorrentDialog *m_ui = nullptr; Ui::AddNewTorrentDialog *m_ui = nullptr;
TorrentContentAdaptor *m_contentAdaptor = nullptr; std::unique_ptr<TorrentContentAdaptor> m_contentAdaptor;
BitTorrent::MagnetUri m_magnetURI; BitTorrent::MagnetUri m_magnetURI;
BitTorrent::TorrentInfo m_torrentInfo; BitTorrent::TorrentInfo m_torrentInfo;
int m_savePathIndex = -1; int m_savePathIndex = -1;

View file

@ -63,7 +63,7 @@ namespace
// qBittorrent section // qBittorrent section
QBITTORRENT_HEADER, QBITTORRENT_HEADER,
RESUME_DATA_STORAGE, RESUME_DATA_STORAGE,
#ifdef QBT_USES_LIBTORRENT2 #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
MEMORY_WORKING_SET_LIMIT, MEMORY_WORKING_SET_LIMIT,
#endif #endif
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
@ -195,7 +195,7 @@ void AdvancedSettings::saveAdvancedSettings() const
BitTorrent::Session *const session = BitTorrent::Session::instance(); BitTorrent::Session *const session = BitTorrent::Session::instance();
session->setResumeDataStorageType(m_comboBoxResumeDataStorage.currentData().value<BitTorrent::ResumeDataStorageType>()); session->setResumeDataStorageType(m_comboBoxResumeDataStorage.currentData().value<BitTorrent::ResumeDataStorageType>());
#ifdef QBT_USES_LIBTORRENT2 #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
// Physical memory (RAM) usage limit // Physical memory (RAM) usage limit
app()->setMemoryWorkingSetLimit(m_spinBoxMemoryWorkingSetLimit.value()); app()->setMemoryWorkingSetLimit(m_spinBoxMemoryWorkingSetLimit.value());
#endif #endif
@ -451,7 +451,7 @@ void AdvancedSettings::loadAdvancedSettings()
m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType()))); m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType())));
addRow(RESUME_DATA_STORAGE, tr("Resume data storage type (requires restart)"), &m_comboBoxResumeDataStorage); 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 // Physical memory (RAM) usage limit
m_spinBoxMemoryWorkingSetLimit.setMinimum(1); m_spinBoxMemoryWorkingSetLimit.setMinimum(1);
m_spinBoxMemoryWorkingSetLimit.setMaximum(std::numeric_limits<int>::max()); m_spinBoxMemoryWorkingSetLimit.setMaximum(std::numeric_limits<int>::max());

View file

@ -30,6 +30,7 @@
#include <libtorrent/config.hpp> #include <libtorrent/config.hpp>
#include <QtGlobal>
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox> #include <QComboBox>
#include <QLineEdit> #include <QLineEdit>
@ -88,7 +89,11 @@ private:
QCheckBox m_checkBoxCoalesceRW; QCheckBox m_checkBoxCoalesceRW;
#else #else
QComboBox m_comboBoxDiskIOType; 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 #endif
#if defined(QBT_USES_LIBTORRENT2) && TORRENT_USE_I2P #if defined(QBT_USES_LIBTORRENT2) && TORRENT_USE_I2P

View file

@ -50,7 +50,7 @@ IPSubnetWhitelistOptionsDialog::IPSubnetWhitelistOptionsDialog(QWidget *parent)
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
QStringList authSubnetWhitelistStringList; 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); authSubnetWhitelistStringList << Utils::Net::subnetToString(subnet);
m_model = new QStringListModel(authSubnetWhitelistStringList, this); 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 // Operate on the m_sortFilter to grab the strings in sorted order
for (int i = 0; i < m_sortFilter->rowCount(); ++i) for (int i = 0; i < m_sortFilter->rowCount(); ++i)
subnets.append(m_sortFilter->index(i, 0).data().toString()); subnets.append(m_sortFilter->index(i, 0).data().toString());
Preferences::instance()->setWebUiAuthSubnetWhitelist(subnets); Preferences::instance()->setWebUIAuthSubnetWhitelist(subnets);
QDialog::accept(); QDialog::accept();
} }
else else

View file

@ -69,6 +69,7 @@
#include "utils.h" #include "utils.h"
#include "watchedfolderoptionsdialog.h" #include "watchedfolderoptionsdialog.h"
#include "watchedfoldersmodel.h" #include "watchedfoldersmodel.h"
#include "webui/webui.h"
#ifndef DISABLE_WEBUI #ifndef DISABLE_WEBUI
#include "base/net/dnsupdater.h" #include "base/net/dnsupdater.h"
@ -80,6 +81,9 @@
#define SETTINGS_KEY(name) u"OptionsDialog/" name #define SETTINGS_KEY(name) u"OptionsDialog/" name
const int WEBUI_MIN_USERNAME_LENGTH = 3;
const int WEBUI_MIN_PASSWORD_LENGTH = 6;
namespace namespace
{ {
QStringList translatedWeekdayNames() 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 // 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. // type casts and that is why we declare required member pointer here instead.
void (QComboBox::*qComboBoxCurrentIndexChanged)(int) = &QComboBox::currentIndexChanged; void (QComboBox::*qComboBoxCurrentIndexChanged)(int) = &QComboBox::currentIndexChanged;
@ -175,7 +189,11 @@ OptionsDialog::OptionsDialog(IGUIApplication *app, QWidget *parent)
// setup apply button // setup apply button
m_applyButton->setEnabled(false); 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 // disable mouse wheel event on widgets to avoid misselection
auto *wheelEventEater = new WheelEventEater(this); auto *wheelEventEater = new WheelEventEater(this);
@ -1211,28 +1229,33 @@ void OptionsDialog::loadWebUITabOptions()
m_ui->textWebUIRootFolder->setMode(FileSystemPathEdit::Mode::DirectoryOpen); m_ui->textWebUIRootFolder->setMode(FileSystemPathEdit::Mode::DirectoryOpen);
m_ui->textWebUIRootFolder->setDialogCaption(tr("Choose Alternative UI files location")); m_ui->textWebUIRootFolder->setDialogCaption(tr("Choose Alternative UI files location"));
m_ui->checkWebUi->setChecked(pref->isWebUiEnabled()); if (app()->webUI()->isErrored())
m_ui->textWebUiAddress->setText(pref->getWebUiAddress()); m_ui->labelWebUIError->setText(tr("WebUI configuration failed. Reason: %1").arg(app()->webUI()->errorMessage()));
m_ui->spinWebUiPort->setValue(pref->getWebUiPort()); 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->checkWebUIUPnP->setChecked(pref->useUPnPForWebUIPort());
m_ui->checkWebUiHttps->setChecked(pref->isWebUiHttpsEnabled()); m_ui->checkWebUIHttps->setChecked(pref->isWebUIHttpsEnabled());
webUIHttpsCertChanged(pref->getWebUIHttpsCertificatePath()); webUIHttpsCertChanged(pref->getWebUIHttpsCertificatePath());
webUIHttpsKeyChanged(pref->getWebUIHttpsKeyPath()); webUIHttpsKeyChanged(pref->getWebUIHttpsKeyPath());
m_ui->textWebUiUsername->setText(pref->getWebUiUsername()); m_ui->textWebUIUsername->setText(pref->getWebUIUsername());
m_ui->checkBypassLocalAuth->setChecked(!pref->isWebUiLocalAuthEnabled()); m_ui->checkBypassLocalAuth->setChecked(!pref->isWebUILocalAuthEnabled());
m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUiAuthSubnetWhitelistEnabled()); m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUIAuthSubnetWhitelistEnabled());
m_ui->IPSubnetWhitelistButton->setEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked()); m_ui->IPSubnetWhitelistButton->setEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
m_ui->spinBanCounter->setValue(pref->getWebUIMaxAuthFailCount()); m_ui->spinBanCounter->setValue(pref->getWebUIMaxAuthFailCount());
m_ui->spinBanDuration->setValue(pref->getWebUIBanDuration().count()); m_ui->spinBanDuration->setValue(pref->getWebUIBanDuration().count());
m_ui->spinSessionTimeout->setValue(pref->getWebUISessionTimeout()); m_ui->spinSessionTimeout->setValue(pref->getWebUISessionTimeout());
// Alternative UI // Alternative UI
m_ui->groupAltWebUI->setChecked(pref->isAltWebUiEnabled()); m_ui->groupAltWebUI->setChecked(pref->isAltWebUIEnabled());
m_ui->textWebUIRootFolder->setSelectedPath(pref->getWebUiRootFolder()); m_ui->textWebUIRootFolder->setSelectedPath(pref->getWebUIRootFolder());
// Security // Security
m_ui->checkClickjacking->setChecked(pref->isWebUiClickjackingProtectionEnabled()); m_ui->checkClickjacking->setChecked(pref->isWebUIClickjackingProtectionEnabled());
m_ui->checkCSRFProtection->setChecked(pref->isWebUiCSRFProtectionEnabled()); m_ui->checkCSRFProtection->setChecked(pref->isWebUICSRFProtectionEnabled());
m_ui->checkSecureCookie->setEnabled(pref->isWebUiHttpsEnabled()); m_ui->checkSecureCookie->setEnabled(pref->isWebUIHttpsEnabled());
m_ui->checkSecureCookie->setChecked(pref->isWebUiSecureCookieEnabled()); m_ui->checkSecureCookie->setChecked(pref->isWebUISecureCookieEnabled());
m_ui->groupHostHeaderValidation->setChecked(pref->isWebUIHostHeaderValidationEnabled()); m_ui->groupHostHeaderValidation->setChecked(pref->isWebUIHostHeaderValidationEnabled());
m_ui->textServerDomains->setText(pref->getServerDomains()); m_ui->textServerDomains->setText(pref->getServerDomains());
// Custom HTTP headers // Custom HTTP headers
@ -1248,18 +1271,18 @@ void OptionsDialog::loadWebUITabOptions()
m_ui->DNSUsernameTxt->setText(pref->getDynDNSUsername()); m_ui->DNSUsernameTxt->setText(pref->getDynDNSUsername());
m_ui->DNSPasswordTxt->setText(pref->getDynDNSPassword()); m_ui->DNSPasswordTxt->setText(pref->getDynDNSPassword());
connect(m_ui->checkWebUi, &QGroupBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkWebUI, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUiAddress, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->textWebUIAddress, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->spinWebUiPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); connect(m_ui->spinWebUIPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkWebUIUPnP, &QAbstractButton::toggled, 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, &ThisType::enableApplyButton);
connect(m_ui->textWebUIHttpsCert, &FileSystemPathLineEdit::selectedPathChanged, this, &OptionsDialog::webUIHttpsCertChanged); 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, &ThisType::enableApplyButton);
connect(m_ui->textWebUIHttpsKey, &FileSystemPathLineEdit::selectedPathChanged, this, &OptionsDialog::webUIHttpsKeyChanged); connect(m_ui->textWebUIHttpsKey, &FileSystemPathLineEdit::selectedPathChanged, this, &OptionsDialog::webUIHttpsKeyChanged);
connect(m_ui->textWebUiUsername, &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->textWebUIPassword, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkBypassLocalAuth, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkBypassLocalAuth, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkBypassAuthSubnetWhitelist, &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->checkClickjacking, &QCheckBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkCSRFProtection, &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->checkSecureCookie, &QCheckBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->groupHostHeaderValidation, &QGroupBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->groupHostHeaderValidation, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->textServerDomains, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->textServerDomains, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
@ -1295,29 +1318,32 @@ void OptionsDialog::saveWebUITabOptions() const
{ {
auto *pref = Preferences::instance(); auto *pref = Preferences::instance();
pref->setWebUiEnabled(isWebUiEnabled()); const bool webUIEnabled = isWebUIEnabled();
pref->setWebUiAddress(m_ui->textWebUiAddress->text());
pref->setWebUiPort(m_ui->spinWebUiPort->value()); pref->setWebUIEnabled(webUIEnabled);
pref->setWebUIAddress(m_ui->textWebUIAddress->text());
pref->setWebUIPort(m_ui->spinWebUIPort->value());
pref->setUPnPForWebUIPort(m_ui->checkWebUIUPnP->isChecked()); 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->setWebUIHttpsCertificatePath(m_ui->textWebUIHttpsCert->selectedPath());
pref->setWebUIHttpsKeyPath(m_ui->textWebUIHttpsKey->selectedPath()); pref->setWebUIHttpsKeyPath(m_ui->textWebUIHttpsKey->selectedPath());
pref->setWebUIMaxAuthFailCount(m_ui->spinBanCounter->value()); pref->setWebUIMaxAuthFailCount(m_ui->spinBanCounter->value());
pref->setWebUIBanDuration(std::chrono::seconds {m_ui->spinBanDuration->value()}); pref->setWebUIBanDuration(std::chrono::seconds {m_ui->spinBanDuration->value()});
pref->setWebUISessionTimeout(m_ui->spinSessionTimeout->value()); pref->setWebUISessionTimeout(m_ui->spinSessionTimeout->value());
// Authentication // Authentication
pref->setWebUiUsername(webUiUsername()); if (const QString username = webUIUsername(); isValidWebUIUsername(username))
if (!webUiPassword().isEmpty()) pref->setWebUIUsername(username);
pref->setWebUIPassword(Utils::Password::PBKDF2::generate(webUiPassword())); if (const QString password = webUIPassword(); isValidWebUIPassword(password))
pref->setWebUiLocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked()); pref->setWebUIPassword(Utils::Password::PBKDF2::generate(password));
pref->setWebUiAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked()); pref->setWebUILocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked());
pref->setWebUIAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
// Alternative UI // Alternative UI
pref->setAltWebUiEnabled(m_ui->groupAltWebUI->isChecked()); pref->setAltWebUIEnabled(m_ui->groupAltWebUI->isChecked());
pref->setWebUiRootFolder(m_ui->textWebUIRootFolder->selectedPath()); pref->setWebUIRootFolder(m_ui->textWebUIRootFolder->selectedPath());
// Security // Security
pref->setWebUiClickjackingProtectionEnabled(m_ui->checkClickjacking->isChecked()); pref->setWebUIClickjackingProtectionEnabled(m_ui->checkClickjacking->isChecked());
pref->setWebUiCSRFProtectionEnabled(m_ui->checkCSRFProtection->isChecked()); pref->setWebUICSRFProtectionEnabled(m_ui->checkCSRFProtection->isChecked());
pref->setWebUiSecureCookieEnabled(m_ui->checkSecureCookie->isChecked()); pref->setWebUISecureCookieEnabled(m_ui->checkSecureCookie->isChecked());
pref->setWebUIHostHeaderValidationEnabled(m_ui->groupHostHeaderValidation->isChecked()); pref->setWebUIHostHeaderValidationEnabled(m_ui->groupHostHeaderValidation->isChecked());
pref->setServerDomains(m_ui->textServerDomains->text()); pref->setServerDomains(m_ui->textServerDomains->text());
// Custom HTTP headers // Custom HTTP headers
@ -1517,53 +1543,37 @@ void OptionsDialog::on_buttonBox_accepted()
{ {
if (m_applyButton->isEnabled()) if (m_applyButton->isEnabled())
{ {
if (!schedTimesOk()) if (!applySettings())
{
m_ui->tabSelection->setCurrentRow(TAB_SPEED);
return; 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); m_applyButton->setEnabled(false);
saveOptions();
} }
accept(); accept();
} }
void OptionsDialog::applySettings() bool OptionsDialog::applySettings()
{ {
if (!schedTimesOk()) if (!schedTimesOk())
{ {
m_ui->tabSelection->setCurrentRow(TAB_SPEED); m_ui->tabSelection->setCurrentRow(TAB_SPEED);
return; return false;
} }
#ifndef DISABLE_WEBUI #ifndef DISABLE_WEBUI
if (!webUIAuthenticationOk()) if (isWebUIEnabled() && !webUIAuthenticationOk())
{ {
m_ui->tabSelection->setCurrentRow(TAB_WEBUI); m_ui->tabSelection->setCurrentRow(TAB_WEBUI);
return; return false;
} }
if (!isAlternativeWebUIPathValid()) if (!isAlternativeWebUIPathValid())
{ {
m_ui->tabSelection->setCurrentRow(TAB_WEBUI); m_ui->tabSelection->setCurrentRow(TAB_WEBUI);
return; return false;
} }
#endif #endif
m_applyButton->setEnabled(false);
saveOptions(); saveOptions();
return true;
} }
void OptionsDialog::on_buttonBox_rejected() void OptionsDialog::on_buttonBox_rejected()
@ -1859,29 +1869,31 @@ void OptionsDialog::webUIHttpsKeyChanged(const Path &path)
(isKeyValid ? u"security-high"_s : u"security-low"_s), 24)); (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() bool OptionsDialog::webUIAuthenticationOk()
{ {
if (webUiUsername().length() < 3) if (!isValidWebUIUsername(webUIUsername()))
{ {
QMessageBox::warning(this, tr("Length Error"), tr("The WebUI 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; 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 WebUI 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 false;

View file

@ -88,7 +88,6 @@ private slots:
void adjustProxyOptions(); void adjustProxyOptions();
void on_buttonBox_accepted(); void on_buttonBox_accepted();
void on_buttonBox_rejected(); void on_buttonBox_rejected();
void applySettings();
void enableApplyButton(); void enableApplyButton();
void toggleComboRatioLimitAct(); void toggleComboRatioLimitAct();
void changePage(QListWidgetItem *, QListWidgetItem *); void changePage(QListWidgetItem *, QListWidgetItem *);
@ -115,6 +114,7 @@ private:
void showEvent(QShowEvent *e) override; void showEvent(QShowEvent *e) override;
// Methods // Methods
bool applySettings();
void saveOptions() const; void saveOptions() const;
void loadBehaviorTabOptions(); void loadBehaviorTabOptions();
@ -184,9 +184,9 @@ private:
int getMaxActiveTorrents() const; int getMaxActiveTorrents() const;
// WebUI // WebUI
#ifndef DISABLE_WEBUI #ifndef DISABLE_WEBUI
bool isWebUiEnabled() const; bool isWebUIEnabled() const;
QString webUiUsername() const; QString webUIUsername() const;
QString webUiPassword() const; QString webUIPassword() const;
bool webUIAuthenticationOk(); bool webUIAuthenticationOk();
bool isAlternativeWebUIPathValid(); bool isAlternativeWebUIPathValid();
#endif #endif

View file

@ -3223,8 +3223,8 @@ Disable encryption: Only connect to peers without protocol encryption</string>
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tabWebuiPage"> <widget class="QWidget" name="tabWebUIPage">
<layout class="QVBoxLayout" name="tabWebuiPageLayout"> <layout class="QVBoxLayout" name="tabWebUIPageLayout">
<property name="leftMargin"> <property name="leftMargin">
<number>0</number> <number>0</number>
</property> </property>
@ -3253,7 +3253,7 @@ Disable encryption: Only connect to peers without protocol encryption</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_23"> <layout class="QVBoxLayout" name="verticalLayout_23">
<item> <item>
<widget class="QGroupBox" name="checkWebUi"> <widget class="QGroupBox" name="checkWebUI">
<property name="title"> <property name="title">
<string>Web User Interface (Remote control)</string> <string>Web User Interface (Remote control)</string>
</property> </property>
@ -3264,17 +3264,29 @@ Disable encryption: Only connect to peers without protocol encryption</string>
<bool>false</bool> <bool>false</bool>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="labelWebUIError">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item> <item>
<widget class="QLabel" name="lblWebUiAddress"> <widget class="QLabel" name="lblWebUIAddress">
<property name="text"> <property name="text">
<string>IP address:</string> <string>IP address:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLineEdit" name="textWebUiAddress"> <widget class="QLineEdit" name="textWebUIAddress">
<property name="toolTip"> <property name="toolTip">
<string>IP address that the Web UI will bind to. <string>IP address that the Web UI will bind to.
Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv4 address, Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv4 address,
@ -3283,14 +3295,14 @@ Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="lblWebUiPort"> <widget class="QLabel" name="lblWebUIPort">
<property name="text"> <property name="text">
<string>Port:</string> <string>Port:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QSpinBox" name="spinWebUiPort"> <widget class="QSpinBox" name="spinWebUIPort">
<property name="minimum"> <property name="minimum">
<number>1</number> <number>1</number>
</property> </property>
@ -3315,7 +3327,7 @@ Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="checkWebUiHttps"> <widget class="QGroupBox" name="checkWebUIHttps">
<property name="title"> <property name="title">
<string>&amp;Use HTTPS instead of HTTP</string> <string>&amp;Use HTTPS instead of HTTP</string>
</property> </property>
@ -3327,14 +3339,14 @@ Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv
</property> </property>
<layout class="QGridLayout" name="gridLayout_11"> <layout class="QGridLayout" name="gridLayout_11">
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLabel" name="lblWebUiKey"> <widget class="QLabel" name="lblWebUIKey">
<property name="text"> <property name="text">
<string>Key:</string> <string>Key:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QLabel" name="lblWebUiCrt"> <widget class="QLabel" name="lblWebUICrt">
<property name="text"> <property name="text">
<string>Certificate:</string> <string>Certificate:</string>
</property> </property>
@ -3366,7 +3378,7 @@ Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="groupWebUiAuth"> <widget class="QGroupBox" name="groupWebUIAuth">
<property name="title"> <property name="title">
<string>Authentication</string> <string>Authentication</string>
</property> </property>
@ -3374,24 +3386,24 @@ Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv
<item> <item>
<layout class="QGridLayout" name="gridLayout_8"> <layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="lblWebUiUsername"> <widget class="QLabel" name="lblWebUIUsername">
<property name="text"> <property name="text">
<string>Username:</string> <string>Username:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QLineEdit" name="textWebUiUsername"/> <widget class="QLineEdit" name="textWebUIUsername"/>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="lblWebUiPassword"> <widget class="QLabel" name="lblWebUIPassword">
<property name="text"> <property name="text">
<string>Password:</string> <string>Password:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLineEdit" name="textWebUiPassword"> <widget class="QLineEdit" name="textWebUIPassword">
<property name="echoMode"> <property name="echoMode">
<enum>QLineEdit::Password</enum> <enum>QLineEdit::Password</enum>
</property> </property>
@ -3819,13 +3831,13 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string>
<tabstop>stopConditionComboBox</tabstop> <tabstop>stopConditionComboBox</tabstop>
<tabstop>spinPort</tabstop> <tabstop>spinPort</tabstop>
<tabstop>checkUPnP</tabstop> <tabstop>checkUPnP</tabstop>
<tabstop>textWebUiUsername</tabstop> <tabstop>textWebUIUsername</tabstop>
<tabstop>checkWebUi</tabstop> <tabstop>checkWebUI</tabstop>
<tabstop>textSavePath</tabstop> <tabstop>textSavePath</tabstop>
<tabstop>scrollArea_7</tabstop> <tabstop>scrollArea_7</tabstop>
<tabstop>scrollArea_2</tabstop> <tabstop>scrollArea_2</tabstop>
<tabstop>spinWebUiPort</tabstop> <tabstop>spinWebUIPort</tabstop>
<tabstop>textWebUiPassword</tabstop> <tabstop>textWebUIPassword</tabstop>
<tabstop>buttonBox</tabstop> <tabstop>buttonBox</tabstop>
<tabstop>tabSelection</tabstop> <tabstop>tabSelection</tabstop>
<tabstop>scrollArea</tabstop> <tabstop>scrollArea</tabstop>
@ -3915,7 +3927,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string>
<tabstop>spinMaxActiveUploads</tabstop> <tabstop>spinMaxActiveUploads</tabstop>
<tabstop>spinMaxActiveTorrents</tabstop> <tabstop>spinMaxActiveTorrents</tabstop>
<tabstop>checkWebUIUPnP</tabstop> <tabstop>checkWebUIUPnP</tabstop>
<tabstop>checkWebUiHttps</tabstop> <tabstop>checkWebUIHttps</tabstop>
<tabstop>checkBypassLocalAuth</tabstop> <tabstop>checkBypassLocalAuth</tabstop>
<tabstop>checkBypassAuthSubnetWhitelist</tabstop> <tabstop>checkBypassAuthSubnetWhitelist</tabstop>
<tabstop>IPSubnetWhitelistButton</tabstop> <tabstop>IPSubnetWhitelistButton</tabstop>

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -30,13 +31,12 @@
#include <QModelIndex> #include <QModelIndex>
#include <QPainter> #include <QPainter>
#include <QStyleOptionViewItem>
#include "base/utils/misc.h" #include "base/utils/string.h"
#include "previewselectdialog.h" #include "previewselectdialog.h"
PreviewListDelegate::PreviewListDelegate(QObject *parent) PreviewListDelegate::PreviewListDelegate(QObject *parent)
: QItemDelegate(parent) : QStyledItemDelegate(parent)
{ {
} }
@ -44,15 +44,8 @@ void PreviewListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &o
{ {
painter->save(); painter->save();
QStyleOptionViewItem opt = QItemDelegate::setOptions(index, option);
drawBackground(painter, opt, index);
switch (index.column()) switch (index.column())
{ {
case PreviewSelectDialog::SIZE:
QItemDelegate::drawDisplay(painter, opt, option.rect, Utils::Misc::friendlyUnit(index.data().toLongLong()));
break;
case PreviewSelectDialog::PROGRESS: case PreviewSelectDialog::PROGRESS:
{ {
const qreal progress = (index.data().toReal() * 100); const qreal progress = (index.data().toReal() * 100);
@ -65,7 +58,7 @@ void PreviewListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &o
break; break;
default: default:
QItemDelegate::paint(painter, option, index); QStyledItemDelegate::paint(painter, option, index);
break; break;
} }

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -28,11 +29,11 @@
#pragma once #pragma once
#include <QItemDelegate> #include <QStyledItemDelegate>
#include "progressbarpainter.h" #include "progressbarpainter.h"
class PreviewListDelegate final : public QItemDelegate class PreviewListDelegate final : public QStyledItemDelegate
{ {
Q_OBJECT Q_OBJECT
Q_DISABLE_COPY_MOVE(PreviewListDelegate) Q_DISABLE_COPY_MOVE(PreviewListDelegate)

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * 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(); const Preferences *pref = Preferences::instance();
// Preview list // Preview list
m_previewListModel = new QStandardItemModel(0, NB_COLUMNS, this); auto *previewListModel = new QStandardItemModel(0, NB_COLUMNS, this);
m_previewListModel->setHeaderData(NAME, Qt::Horizontal, tr("Name")); previewListModel->setHeaderData(NAME, Qt::Horizontal, tr("Name"));
m_previewListModel->setHeaderData(SIZE, Qt::Horizontal, tr("Size")); previewListModel->setHeaderData(SIZE, Qt::Horizontal, tr("Size"));
m_previewListModel->setHeaderData(PROGRESS, Qt::Horizontal, tr("Progress")); previewListModel->setHeaderData(PROGRESS, Qt::Horizontal, tr("Progress"));
m_ui->previewList->setAlternatingRowColors(pref->useAlternatingRowColors()); 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_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 // Fill list in
const QVector<qreal> fp = torrent->filesProgress(); const QVector<qreal> fp = torrent->filesProgress();
for (int i = 0; i < torrent->filesCount(); ++i) 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); const Path filePath = torrent->filePath(i);
if (Utils::Misc::isPreviewable(filePath)) if (Utils::Misc::isPreviewable(filePath))
{ {
int row = m_previewListModel->rowCount(); int row = previewListModel->rowCount();
m_previewListModel->insertRow(row); previewListModel->insertRow(row);
m_previewListModel->setData(m_previewListModel->index(row, NAME), filePath.filename()); previewListModel->setData(previewListModel->index(row, NAME), filePath.filename());
m_previewListModel->setData(m_previewListModel->index(row, SIZE), torrent->fileSize(i)); previewListModel->setData(previewListModel->index(row, SIZE), Utils::Misc::friendlyUnit(torrent->fileSize(i)));
m_previewListModel->setData(m_previewListModel->index(row, PROGRESS), fp[i]); previewListModel->setData(previewListModel->index(row, PROGRESS), fp[i]);
m_previewListModel->setData(m_previewListModel->index(row, FILE_INDEX), 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()->setContextMenuPolicy(Qt::CustomContextMenu);
m_ui->previewList->header()->setFirstSectionMovable(true); m_ui->previewList->header()->setFirstSectionMovable(true);
m_ui->previewList->header()->setSortIndicator(0, Qt::AscendingOrder); 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); connect(m_ui->previewList->header(), &QWidget::customContextMenuRequested, this, &PreviewSelectDialog::displayColumnHeaderMenu);
@ -129,7 +133,7 @@ void PreviewSelectDialog::previewButtonClicked()
// File // File
if (!path.exists()) 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; QWidget *parent = isSingleFile ? this->parentWidget() : this;
QMessageBox::critical(parent, tr("Preview impossible") QMessageBox::critical(parent, tr("Preview impossible")
, tr("Sorry, we can't preview this file: \"%1\".").arg(path.toString())); , 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 // Only one file, no choice
if (m_previewListModel->rowCount() <= 1) if (m_ui->previewList->model()->rowCount() <= 1)
previewButtonClicked(); previewButtonClicked();
} }

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -33,8 +34,6 @@
#include "base/path.h" #include "base/path.h"
#include "base/settingvalue.h" #include "base/settingvalue.h"
class QStandardItemModel;
namespace BitTorrent namespace BitTorrent
{ {
class Torrent; class Torrent;
@ -44,7 +43,6 @@ namespace Ui
{ {
class PreviewSelectDialog; class PreviewSelectDialog;
} }
class PreviewListDelegate;
class PreviewSelectDialog final : public QDialog class PreviewSelectDialog final : public QDialog
{ {
@ -79,8 +77,6 @@ private:
void saveWindowState(); void saveWindowState();
Ui::PreviewSelectDialog *m_ui = nullptr; Ui::PreviewSelectDialog *m_ui = nullptr;
QStandardItemModel *m_previewListModel = nullptr;
PreviewListDelegate *m_listDelegate = nullptr;
const BitTorrent::Torrent *m_torrent = nullptr; const BitTorrent::Torrent *m_torrent = nullptr;
bool m_headerStateInitialized = false; bool m_headerStateInitialized = false;

View file

@ -82,6 +82,7 @@ PropertiesWidget::PropertiesWidget(QWidget *parent)
m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine); m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine);
m_ui->filesList->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Open); m_ui->filesList->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Open);
m_ui->filesList->setOpenByEnterKey(true);
// SIGNAL/SLOTS // SIGNAL/SLOTS
connect(m_ui->selectAllButton, &QPushButton::clicked, m_ui->filesList, &TorrentContentWidget::checkAll); connect(m_ui->selectAllButton, &QPushButton::clicked, m_ui->filesList, &TorrentContentWidget::checkAll);

View file

@ -192,7 +192,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="3" colspan="4"> <item row="2" column="3">
<widget class="QLabel" name="labelUpSpeedVal"> <widget class="QLabel" name="labelUpSpeedVal">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred"> <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@ -382,7 +382,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="3" colspan="4"> <item row="1" column="3">
<widget class="QLabel" name="labelUpTotalVal"> <widget class="QLabel" name="labelUpTotalVal">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred"> <sizepolicy hsizetype="Expanding" vsizetype="Preferred">

View file

@ -89,10 +89,6 @@ TorrentContentWidget::TorrentContentWidget(QWidget *parent)
const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut); const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut);
connect(renameFileHotkey, &QShortcut::activated, this, &TorrentContentWidget::renameSelectedFile); 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); connect(model(), &QAbstractItemModel::modelReset, this, &TorrentContentWidget::expandRecursively);
} }
@ -118,6 +114,32 @@ void TorrentContentWidget::refresh()
setUpdatesEnabled(true); 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 TorrentContentWidget::DoubleClickAction TorrentContentWidget::doubleClickAction() const
{ {
return m_doubleClickAction; return m_doubleClickAction;

View file

@ -34,6 +34,8 @@
#include "base/bittorrent/downloadpriority.h" #include "base/bittorrent/downloadpriority.h"
#include "base/pathfwd.h" #include "base/pathfwd.h"
class QShortcut;
namespace BitTorrent namespace BitTorrent
{ {
class Torrent; class Torrent;
@ -78,6 +80,9 @@ public:
BitTorrent::TorrentContentHandler *contentHandler() const; BitTorrent::TorrentContentHandler *contentHandler() const;
void refresh(); void refresh();
bool openByEnterKey() const;
void setOpenByEnterKey(bool value);
DoubleClickAction doubleClickAction() const; DoubleClickAction doubleClickAction() const;
void setDoubleClickAction(DoubleClickAction action); void setDoubleClickAction(DoubleClickAction action);
@ -118,4 +123,6 @@ private:
TorrentContentFilterModel *m_filterModel; TorrentContentFilterModel *m_filterModel;
DoubleClickAction m_doubleClickAction = DoubleClickAction::Rename; DoubleClickAction m_doubleClickAction = DoubleClickAction::Rename;
ColumnsVisibilityMode m_columnsVisibilityMode = ColumnsVisibilityMode::Editable; ColumnsVisibilityMode m_columnsVisibilityMode = ColumnsVisibilityMode::Editable;
QShortcut *m_openFileHotkeyEnter = nullptr;
QShortcut *m_openFileHotkeyReturn = nullptr;
}; };

View file

@ -117,6 +117,7 @@ TransferListModel::TransferListModel(QObject *parent)
, m_completedIcon {UIThemeManager::instance()->getIcon(u"checked-completed"_s, u"completed"_s)} , m_completedIcon {UIThemeManager::instance()->getIcon(u"checked-completed"_s, u"completed"_s)}
, m_downloadingIcon {UIThemeManager::instance()->getIcon(u"downloading"_s)} , m_downloadingIcon {UIThemeManager::instance()->getIcon(u"downloading"_s)}
, m_errorIcon {UIThemeManager::instance()->getIcon(u"error"_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_pausedIcon {UIThemeManager::instance()->getIcon(u"stopped"_s, u"media-playback-pause"_s)}
, m_queuedIcon {UIThemeManager::instance()->getIcon(u"queued"_s)} , m_queuedIcon {UIThemeManager::instance()->getIcon(u"queued"_s)}
, m_stalledDLIcon {UIThemeManager::instance()->getIcon(u"stalledDL"_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::CheckingDownloading:
case BitTorrent::TorrentState::CheckingUploading: case BitTorrent::TorrentState::CheckingUploading:
case BitTorrent::TorrentState::CheckingResumeData: case BitTorrent::TorrentState::CheckingResumeData:
case BitTorrent::TorrentState::Moving:
return m_checkingIcon; return m_checkingIcon;
case BitTorrent::TorrentState::Moving:
return m_movingIcon;
case BitTorrent::TorrentState::Unknown: case BitTorrent::TorrentState::Unknown:
case BitTorrent::TorrentState::MissingFiles: case BitTorrent::TorrentState::MissingFiles:
case BitTorrent::TorrentState::Error: case BitTorrent::TorrentState::Error:

View file

@ -137,6 +137,7 @@ private:
QIcon m_completedIcon; QIcon m_completedIcon;
QIcon m_downloadingIcon; QIcon m_downloadingIcon;
QIcon m_errorIcon; QIcon m_errorIcon;
QIcon m_movingIcon;
QIcon m_pausedIcon; QIcon m_pausedIcon;
QIcon m_queuedIcon; QIcon m_queuedIcon;
QIcon m_stalledDLIcon; QIcon m_stalledDLIcon;

View file

@ -327,6 +327,7 @@
<file>qbittorrent-tray-dark.svg</file> <file>qbittorrent-tray-dark.svg</file>
<file>qbittorrent-tray-light.svg</file> <file>qbittorrent-tray-light.svg</file>
<file>qbittorrent-tray.svg</file> <file>qbittorrent-tray.svg</file>
<file alias="qbittorrent.svg">qbittorrent-tray.svg</file>
<file>queued.svg</file> <file>queued.svg</file>
<file>ratio.svg</file> <file>ratio.svg</file>
<file>reannounce.svg</file> <file>reannounce.svg</file>

View file

@ -4,7 +4,6 @@ add_library(qbt_webui STATIC
api/apierror.h api/apierror.h
api/appcontroller.h api/appcontroller.h
api/authcontroller.h api/authcontroller.h
api/freediskspacechecker.h
api/isessionmanager.h api/isessionmanager.h
api/logcontroller.h api/logcontroller.h
api/rsscontroller.h api/rsscontroller.h
@ -13,6 +12,7 @@ add_library(qbt_webui STATIC
api/torrentscontroller.h api/torrentscontroller.h
api/transfercontroller.h api/transfercontroller.h
api/serialize/serialize_torrent.h api/serialize/serialize_torrent.h
freediskspacechecker.h
webapplication.h webapplication.h
webui.h webui.h
@ -21,7 +21,6 @@ add_library(qbt_webui STATIC
api/apierror.cpp api/apierror.cpp
api/appcontroller.cpp api/appcontroller.cpp
api/authcontroller.cpp api/authcontroller.cpp
api/freediskspacechecker.cpp
api/logcontroller.cpp api/logcontroller.cpp
api/rsscontroller.cpp api/rsscontroller.cpp
api/searchcontroller.cpp api/searchcontroller.cpp
@ -29,6 +28,7 @@ add_library(qbt_webui STATIC
api/torrentscontroller.cpp api/torrentscontroller.cpp
api/transfercontroller.cpp api/transfercontroller.cpp
api/serialize/serialize_torrent.cpp api/serialize/serialize_torrent.cpp
freediskspacechecker.cpp
webapplication.cpp webapplication.cpp
webui.cpp webui.cpp
) )

View file

@ -193,6 +193,16 @@ void AppController::preferencesAction()
data[u"max_uploads"_s] = session->maxUploads(); data[u"max_uploads"_s] = session->maxUploads();
data[u"max_uploads_per_torrent"_s] = session->maxUploadsPerTorrent(); 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 // Proxy Server
const auto *proxyManager = Net::ProxyConfigurationManager::instance(); const auto *proxyManager = Net::ProxyConfigurationManager::instance();
Net::ProxyConfiguration proxyConf = proxyManager->proxyConfiguration(); Net::ProxyConfiguration proxyConf = proxyManager->proxyConfiguration();
@ -268,30 +278,30 @@ void AppController::preferencesAction()
// WebUI // WebUI
// HTTP Server // HTTP Server
data[u"web_ui_domain_list"_s] = pref->getServerDomains(); data[u"web_ui_domain_list"_s] = pref->getServerDomains();
data[u"web_ui_address"_s] = pref->getWebUiAddress(); data[u"web_ui_address"_s] = pref->getWebUIAddress();
data[u"web_ui_port"_s] = pref->getWebUiPort(); data[u"web_ui_port"_s] = pref->getWebUIPort();
data[u"web_ui_upnp"_s] = pref->useUPnPForWebUIPort(); 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_cert_path"_s] = pref->getWebUIHttpsCertificatePath().toString();
data[u"web_ui_https_key_path"_s] = pref->getWebUIHttpsKeyPath().toString(); data[u"web_ui_https_key_path"_s] = pref->getWebUIHttpsKeyPath().toString();
// Authentication // Authentication
data[u"web_ui_username"_s] = pref->getWebUiUsername(); data[u"web_ui_username"_s] = pref->getWebUIUsername();
data[u"bypass_local_auth"_s] = !pref->isWebUiLocalAuthEnabled(); data[u"bypass_local_auth"_s] = !pref->isWebUILocalAuthEnabled();
data[u"bypass_auth_subnet_whitelist_enabled"_s] = pref->isWebUiAuthSubnetWhitelistEnabled(); data[u"bypass_auth_subnet_whitelist_enabled"_s] = pref->isWebUIAuthSubnetWhitelistEnabled();
QStringList authSubnetWhitelistStringList; 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); authSubnetWhitelistStringList << Utils::Net::subnetToString(subnet);
data[u"bypass_auth_subnet_whitelist"_s] = authSubnetWhitelistStringList.join(u'\n'); 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_max_auth_fail_count"_s] = pref->getWebUIMaxAuthFailCount();
data[u"web_ui_ban_duration"_s] = static_cast<int>(pref->getWebUIBanDuration().count()); data[u"web_ui_ban_duration"_s] = static_cast<int>(pref->getWebUIBanDuration().count());
data[u"web_ui_session_timeout"_s] = pref->getWebUISessionTimeout(); data[u"web_ui_session_timeout"_s] = pref->getWebUISessionTimeout();
// Use alternative WebUI // Use alternative WebUI
data[u"alternative_webui_enabled"_s] = pref->isAltWebUiEnabled(); data[u"alternative_webui_enabled"_s] = pref->isAltWebUIEnabled();
data[u"alternative_webui_path"_s] = pref->getWebUiRootFolder().toString(); data[u"alternative_webui_path"_s] = pref->getWebUIRootFolder().toString();
// Security // Security
data[u"web_ui_clickjacking_protection_enabled"_s] = pref->isWebUiClickjackingProtectionEnabled(); data[u"web_ui_clickjacking_protection_enabled"_s] = pref->isWebUIClickjackingProtectionEnabled();
data[u"web_ui_csrf_protection_enabled"_s] = pref->isWebUiCSRFProtectionEnabled(); data[u"web_ui_csrf_protection_enabled"_s] = pref->isWebUICSRFProtectionEnabled();
data[u"web_ui_secure_cookie_enabled"_s] = pref->isWebUiSecureCookieEnabled(); data[u"web_ui_secure_cookie_enabled"_s] = pref->isWebUISecureCookieEnabled();
data[u"web_ui_host_header_validation_enabled"_s] = pref->isWebUIHostHeaderValidationEnabled(); data[u"web_ui_host_header_validation_enabled"_s] = pref->isWebUIHostHeaderValidationEnabled();
// Custom HTTP headers // Custom HTTP headers
data[u"web_ui_use_custom_http_headers_enabled"_s] = pref->isWebUICustomHTTPHeadersEnabled(); data[u"web_ui_use_custom_http_headers_enabled"_s] = pref->isWebUICustomHTTPHeadersEnabled();
@ -628,6 +638,24 @@ void AppController::setPreferencesAction()
if (hasKey(u"max_uploads_per_torrent"_s)) if (hasKey(u"max_uploads_per_torrent"_s))
session->setMaxUploadsPerTorrent(it.value().toInt()); 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 // Proxy Server
auto *proxyManager = Net::ProxyConfigurationManager::instance(); auto *proxyManager = Net::ProxyConfigurationManager::instance();
Net::ProxyConfiguration proxyConf = proxyManager->proxyConfiguration(); Net::ProxyConfiguration proxyConf = proxyManager->proxyConfiguration();
@ -759,30 +787,30 @@ void AppController::setPreferencesAction()
if (hasKey(u"web_ui_domain_list"_s)) if (hasKey(u"web_ui_domain_list"_s))
pref->setServerDomains(it.value().toString()); pref->setServerDomains(it.value().toString());
if (hasKey(u"web_ui_address"_s)) if (hasKey(u"web_ui_address"_s))
pref->setWebUiAddress(it.value().toString()); pref->setWebUIAddress(it.value().toString());
if (hasKey(u"web_ui_port"_s)) if (hasKey(u"web_ui_port"_s))
pref->setWebUiPort(it.value().value<quint16>()); pref->setWebUIPort(it.value().value<quint16>());
if (hasKey(u"web_ui_upnp"_s)) if (hasKey(u"web_ui_upnp"_s))
pref->setUPnPForWebUIPort(it.value().toBool()); pref->setUPnPForWebUIPort(it.value().toBool());
if (hasKey(u"use_https"_s)) 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)) if (hasKey(u"web_ui_https_cert_path"_s))
pref->setWebUIHttpsCertificatePath(Path(it.value().toString())); pref->setWebUIHttpsCertificatePath(Path(it.value().toString()));
if (hasKey(u"web_ui_https_key_path"_s)) if (hasKey(u"web_ui_https_key_path"_s))
pref->setWebUIHttpsKeyPath(Path(it.value().toString())); pref->setWebUIHttpsKeyPath(Path(it.value().toString()));
// Authentication // Authentication
if (hasKey(u"web_ui_username"_s)) if (hasKey(u"web_ui_username"_s))
pref->setWebUiUsername(it.value().toString()); pref->setWebUIUsername(it.value().toString());
if (hasKey(u"web_ui_password"_s)) if (hasKey(u"web_ui_password"_s))
pref->setWebUIPassword(Utils::Password::PBKDF2::generate(it.value().toByteArray())); pref->setWebUIPassword(Utils::Password::PBKDF2::generate(it.value().toByteArray()));
if (hasKey(u"bypass_local_auth"_s)) 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)) 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)) if (hasKey(u"bypass_auth_subnet_whitelist"_s))
{ {
// recognize new lines and commas as delimiters // 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)) if (hasKey(u"web_ui_max_auth_fail_count"_s))
pref->setWebUIMaxAuthFailCount(it.value().toInt()); pref->setWebUIMaxAuthFailCount(it.value().toInt());
@ -792,16 +820,16 @@ void AppController::setPreferencesAction()
pref->setWebUISessionTimeout(it.value().toInt()); pref->setWebUISessionTimeout(it.value().toInt());
// Use alternative WebUI // Use alternative WebUI
if (hasKey(u"alternative_webui_enabled"_s)) if (hasKey(u"alternative_webui_enabled"_s))
pref->setAltWebUiEnabled(it.value().toBool()); pref->setAltWebUIEnabled(it.value().toBool());
if (hasKey(u"alternative_webui_path"_s)) if (hasKey(u"alternative_webui_path"_s))
pref->setWebUiRootFolder(Path(it.value().toString())); pref->setWebUIRootFolder(Path(it.value().toString()));
// Security // Security
if (hasKey(u"web_ui_clickjacking_protection_enabled"_s)) 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)) 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)) 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)) if (hasKey(u"web_ui_host_header_validation_enabled"_s))
pref->setWebUIHostHeaderValidationEnabled(it.value().toBool()); pref->setWebUIHostHeaderValidationEnabled(it.value().toBool());
// Custom HTTP headers // Custom HTTP headers

View file

@ -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() void AuthController::loginAction()
{ {
if (m_sessionManager->session()) if (m_sessionManager->session())
@ -51,9 +61,9 @@ void AuthController::loginAction()
return; return;
} }
const QString clientAddr {m_sessionManager->clientId()}; const QString clientAddr = m_sessionManager->clientId();
const QString usernameFromWeb {params()[u"username"_s]}; const QString usernameFromWeb = params()[u"username"_s];
const QString passwordFromWeb {params()[u"password"_s]}; const QString passwordFromWeb = params()[u"password"_s];
if (isBanned()) if (isBanned())
{ {
@ -64,12 +74,8 @@ void AuthController::loginAction()
, 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 bool usernameEqual = Utils::Password::slowEquals(usernameFromWeb.toUtf8(), m_username.toUtf8());
const bool passwordEqual = Utils::Password::PBKDF2::verify(m_passwordHash, passwordFromWeb);
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);
if (usernameEqual && passwordEqual) if (usernameEqual && passwordEqual)
{ {

View file

@ -28,8 +28,10 @@
#pragma once #pragma once
#include <QByteArray>
#include <QDeadlineTimer> #include <QDeadlineTimer>
#include <QHash> #include <QHash>
#include <QString>
#include "apicontroller.h" #include "apicontroller.h"
@ -45,6 +47,9 @@ class AuthController : public APIController
public: public:
explicit AuthController(ISessionManager *sessionManager, IApplication *app, QObject *parent = nullptr); explicit AuthController(ISessionManager *sessionManager, IApplication *app, QObject *parent = nullptr);
void setUsername(const QString &username);
void setPasswordHash(const QByteArray &passwordHash);
private slots: private slots:
void loginAction(); void loginAction();
void logoutAction() const; void logoutAction() const;
@ -56,6 +61,9 @@ private:
ISessionManager *m_sessionManager = nullptr; ISessionManager *m_sessionManager = nullptr;
QString m_username;
QByteArray m_passwordHash;
struct FailedLogin struct FailedLogin
{ {
int failedAttemptsCount = 0; int failedAttemptsCount = 0;

View file

@ -33,7 +33,6 @@
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QMetaObject> #include <QMetaObject>
#include <QThreadPool>
#include "base/algorithm.h" #include "base/algorithm.h"
#include "base/bittorrent/cachestatus.h" #include "base/bittorrent/cachestatus.h"
@ -50,13 +49,10 @@
#include "base/preferences.h" #include "base/preferences.h"
#include "base/utils/string.h" #include "base/utils/string.h"
#include "apierror.h" #include "apierror.h"
#include "freediskspacechecker.h"
#include "serialize/serialize_torrent.h" #include "serialize/serialize_torrent.h"
namespace namespace
{ {
const int FREEDISKSPACE_CHECK_TIMEOUT = 30000;
// Sync main data keys // Sync main data keys
const QString KEY_SYNC_MAINDATA_QUEUEING = u"queueing"_s; const QString KEY_SYNC_MAINDATA_QUEUEING = u"queueing"_s;
const QString KEY_SYNC_MAINDATA_REFRESH_INTERVAL = u"refresh_interval"_s; const QString KEY_SYNC_MAINDATA_REFRESH_INTERVAL = u"refresh_interval"_s;
@ -391,8 +387,11 @@ namespace
SyncController::SyncController(IApplication *app, QObject *parent) SyncController::SyncController(IApplication *app, QObject *parent)
: APIController(app, 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. // 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 = 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_QUEUEING] = session->isQueueingSystemEnabled();
m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled(); m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled();
m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval(); 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(); m_removedTrackers.clear();
QVariantMap serverState = getTransferInfo(); 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_QUEUEING] = session->isQueueingSystemEnabled();
serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled(); serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled();
serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval(); serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval();
@ -782,34 +781,6 @@ void SyncController::torrentPeersAction()
setResult(generateSyncData(acceptedResponseId, data, m_lastAcceptedPeersResponse, m_lastPeersResponse)); 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) void SyncController::onCategoryAdded(const QString &categoryName)
{ {
m_removedCategories.remove(categoryName); m_removedCategories.remove(categoryName);

View file

@ -28,22 +28,17 @@
#pragma once #pragma once
#include <QElapsedTimer>
#include <QVariantMap>
#include <QSet> #include <QSet>
#include <QVariantMap>
#include "base/bittorrent/infohash.h" #include "base/bittorrent/infohash.h"
#include "apicontroller.h" #include "apicontroller.h"
class QThread;
namespace BitTorrent namespace BitTorrent
{ {
class Torrent; class Torrent;
} }
class FreeDiskSpaceChecker;
class SyncController : public APIController class SyncController : public APIController
{ {
Q_OBJECT Q_OBJECT
@ -54,14 +49,14 @@ public:
explicit SyncController(IApplication *app, QObject *parent = nullptr); explicit SyncController(IApplication *app, QObject *parent = nullptr);
public slots:
void updateFreeDiskSpace(qint64 freeDiskSpace);
private slots: private slots:
void maindataAction(); void maindataAction();
void torrentPeersAction(); void torrentPeersAction();
private: private:
qint64 getFreeDiskSpace();
void invokeChecker();
void makeMaindataSnapshot(); void makeMaindataSnapshot();
QJsonObject generateMaindataSyncData(int id, bool fullUpdate); QJsonObject generateMaindataSyncData(int id, bool fullUpdate);
@ -85,8 +80,6 @@ private:
void onTorrentTrackersChanged(BitTorrent::Torrent *torrent); void onTorrentTrackersChanged(BitTorrent::Torrent *torrent);
qint64 m_freeDiskSpace = 0; qint64 m_freeDiskSpace = 0;
QElapsedTimer m_freeDiskSpaceElapsedTimer;
bool m_isFreeDiskSpaceCheckerRunning = false;
QVariantMap m_lastPeersResponse; QVariantMap m_lastPeersResponse;
QVariantMap m_lastAcceptedPeersResponse; QVariantMap m_lastAcceptedPeersResponse;

View file

@ -232,6 +232,11 @@ namespace
} }
} }
void TorrentsController::countAction()
{
setResult(QString::number(BitTorrent::Session::instance()->torrentsCount()));
}
// Returns all the torrents in JSON format. // Returns all the torrents in JSON format.
// The return value is a JSON-formatted list of dictionaries. // The return value is a JSON-formatted list of dictionaries.
// The dictionary keys are: // The dictionary keys are:

View file

@ -39,6 +39,7 @@ public:
using APIController::APIController; using APIController::APIController;
private slots: private slots:
void countAction();
void infoAction(); void infoAction();
void propertiesAction(); void propertiesAction();
void trackersAction(); void trackersAction();

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com> * Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -31,8 +32,13 @@
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/utils/fs.h" #include "base/utils/fs.h"
qint64 FreeDiskSpaceChecker::lastResult() const
{
return m_lastResult;
}
void FreeDiskSpaceChecker::check() void FreeDiskSpaceChecker::check()
{ {
const qint64 freeDiskSpace = Utils::Fs::freeDiskSpaceOnPath(BitTorrent::Session::instance()->savePath()); m_lastResult = Utils::Fs::freeDiskSpaceOnPath(BitTorrent::Session::instance()->savePath());
emit checked(freeDiskSpace); emit checked(m_lastResult);
} }

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com> * Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -38,9 +39,14 @@ class FreeDiskSpaceChecker final : public QObject
public: public:
using QObject::QObject; using QObject::QObject;
qint64 lastResult() const;
public slots: public slots:
void check(); void check();
signals: signals:
void checked(qint64 freeSpaceSize); void checked(qint64 freeSpaceSize);
private:
qint64 m_lastResult = 0;
}; };

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014, 2022 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2014, 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -29,16 +29,20 @@
#include "webapplication.h" #include "webapplication.h"
#include <algorithm> #include <algorithm>
#include <chrono>
#include <QDateTime> #include <QDateTime>
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QJsonDocument> #include <QJsonDocument>
#include <QMetaObject>
#include <QMimeDatabase> #include <QMimeDatabase>
#include <QMimeType> #include <QMimeType>
#include <QNetworkCookie> #include <QNetworkCookie>
#include <QRegularExpression> #include <QRegularExpression>
#include <QThread>
#include <QTimer>
#include <QUrl> #include <QUrl>
#include "base/algorithm.h" #include "base/algorithm.h"
@ -60,6 +64,7 @@
#include "api/synccontroller.h" #include "api/synccontroller.h"
#include "api/torrentscontroller.h" #include "api/torrentscontroller.h"
#include "api/transfercontroller.h" #include "api/transfercontroller.h"
#include "freediskspacechecker.h"
const int MAX_ALLOWED_FILESIZE = 10 * 1024 * 1024; const int MAX_ALLOWED_FILESIZE = 10 * 1024 * 1024;
const QString DEFAULT_SESSION_COOKIE_NAME = u"SID"_s; 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 PUBLIC_FOLDER = u"/public"_s;
const QString PRIVATE_FOLDER = u"/private"_s; const QString PRIVATE_FOLDER = u"/private"_s;
using namespace std::chrono_literals;
const std::chrono::seconds FREEDISKSPACE_CHECK_TIMEOUT = 30s;
namespace namespace
{ {
QStringMap parseCookie(const QStringView cookieStr) QStringMap parseCookie(const QStringView cookieStr)
@ -147,6 +156,9 @@ WebApplication::WebApplication(IApplication *app, QObject *parent)
, ApplicationComponent(app) , ApplicationComponent(app)
, m_cacheID {QString::number(Utils::Random::rand(), 36)} , m_cacheID {QString::number(Utils::Random::rand(), 36)}
, m_authController {new AuthController(this, app, this)} , 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); declarePublicAPI(u"auth/login"_s);
@ -163,6 +175,16 @@ WebApplication::WebApplication(IApplication *app, QObject *parent)
} }
m_sessionCookieName = DEFAULT_SESSION_COOKIE_NAME; 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() WebApplication::~WebApplication()
@ -269,6 +291,16 @@ const Http::Environment &WebApplication::env() const
return m_env; 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() void WebApplication::doProcessRequest()
{ {
const QRegularExpressionMatch match = m_apiPathPattern.match(request().path); const QRegularExpressionMatch match = m_apiPathPattern.match(request().path);
@ -357,8 +389,8 @@ void WebApplication::configure()
{ {
const auto *pref = Preferences::instance(); const auto *pref = Preferences::instance();
const bool isAltUIUsed = pref->isAltWebUiEnabled(); const bool isAltUIUsed = pref->isAltWebUIEnabled();
const Path rootFolder = (!isAltUIUsed ? Path(WWW_FOLDER) : pref->getWebUiRootFolder()); const Path rootFolder = (!isAltUIUsed ? Path(WWW_FOLDER) : pref->getWebUIRootFolder());
if ((isAltUIUsed != m_isAltUIUsed) || (rootFolder != m_rootFolder)) if ((isAltUIUsed != m_isAltUIUsed) || (rootFolder != m_rootFolder))
{ {
m_isAltUIUsed = isAltUIUsed; m_isAltUIUsed = isAltUIUsed;
@ -388,18 +420,18 @@ void WebApplication::configure()
} }
} }
m_isLocalAuthEnabled = pref->isWebUiLocalAuthEnabled(); m_isLocalAuthEnabled = pref->isWebUILocalAuthEnabled();
m_isAuthSubnetWhitelistEnabled = pref->isWebUiAuthSubnetWhitelistEnabled(); m_isAuthSubnetWhitelistEnabled = pref->isWebUIAuthSubnetWhitelistEnabled();
m_authSubnetWhitelist = pref->getWebUiAuthSubnetWhitelist(); m_authSubnetWhitelist = pref->getWebUIAuthSubnetWhitelist();
m_sessionTimeout = pref->getWebUISessionTimeout(); m_sessionTimeout = pref->getWebUISessionTimeout();
m_domainList = pref->getServerDomains().split(u';', Qt::SkipEmptyParts); m_domainList = pref->getServerDomains().split(u';', Qt::SkipEmptyParts);
std::for_each(m_domainList.begin(), m_domainList.end(), [](QString &entry) { entry = entry.trimmed(); }); std::for_each(m_domainList.begin(), m_domainList.end(), [](QString &entry) { entry = entry.trimmed(); });
m_isCSRFProtectionEnabled = pref->isWebUiCSRFProtectionEnabled(); m_isCSRFProtectionEnabled = pref->isWebUICSRFProtectionEnabled();
m_isSecureCookieEnabled = pref->isWebUiSecureCookieEnabled(); m_isSecureCookieEnabled = pref->isWebUISecureCookieEnabled();
m_isHostHeaderValidationEnabled = pref->isWebUIHostHeaderValidationEnabled(); m_isHostHeaderValidationEnabled = pref->isWebUIHostHeaderValidationEnabled();
m_isHttpsEnabled = pref->isWebUiHttpsEnabled(); m_isHttpsEnabled = pref->isWebUIHttpsEnabled();
m_prebuiltHeaders.clear(); m_prebuiltHeaders.clear();
m_prebuiltHeaders.push_back({Http::HEADER_X_XSS_PROTECTION, u"1; mode=block"_s}); m_prebuiltHeaders.push_back({Http::HEADER_X_XSS_PROTECTION, u"1; mode=block"_s});
@ -411,7 +443,7 @@ void WebApplication::configure()
m_prebuiltHeaders.push_back({Http::HEADER_REFERRER_POLICY, u"same-origin"_s}); m_prebuiltHeaders.push_back({Http::HEADER_REFERRER_POLICY, u"same-origin"_s});
} }
const bool isClickjackingProtectionEnabled = pref->isWebUiClickjackingProtectionEnabled(); const bool isClickjackingProtectionEnabled = pref->isWebUIClickjackingProtectionEnabled();
if (isClickjackingProtectionEnabled) if (isClickjackingProtectionEnabled)
m_prebuiltHeaders.push_back({Http::HEADER_X_FRAME_OPTIONS, u"SAMEORIGIN"_s}); m_prebuiltHeaders.push_back({Http::HEADER_X_FRAME_OPTIONS, u"SAMEORIGIN"_s});
@ -680,14 +712,18 @@ void WebApplication::sessionStart()
}); });
m_currentSession = new WebSession(generateSid(), app()); m_currentSession = new WebSession(generateSid(), app());
m_sessions[m_currentSession->id()] = m_currentSession;
m_currentSession->registerAPIController<AppController>(u"app"_s); m_currentSession->registerAPIController<AppController>(u"app"_s);
m_currentSession->registerAPIController<LogController>(u"log"_s); m_currentSession->registerAPIController<LogController>(u"log"_s);
m_currentSession->registerAPIController<RSSController>(u"rss"_s); m_currentSession->registerAPIController<RSSController>(u"rss"_s);
m_currentSession->registerAPIController<SearchController>(u"search"_s); m_currentSession->registerAPIController<SearchController>(u"search"_s);
m_currentSession->registerAPIController<SyncController>(u"sync"_s);
m_currentSession->registerAPIController<TorrentsController>(u"torrents"_s); m_currentSession->registerAPIController<TorrentsController>(u"torrents"_s);
m_currentSession->registerAPIController<TransferController>(u"transfer"_s); m_currentSession->registerAPIController<TransferController>(u"transfer"_s);
m_sessions[m_currentSession->id()] = m_currentSession;
auto *syncController = m_currentSession->registerAPIController<SyncController>(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()}; QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toUtf8()};
cookie.setHttpOnly(true); cookie.setHttpOnly(true);

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014, 2017, 2022 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2014, 2017, 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -49,13 +49,17 @@
#include "base/http/types.h" #include "base/http/types.h"
#include "base/path.h" #include "base/path.h"
#include "base/utils/net.h" #include "base/utils/net.h"
#include "base/utils/thread.h"
#include "base/utils/version.h" #include "base/utils/version.h"
#include "api/isessionmanager.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 QTimer;
class APIController; class APIController;
class AuthController; class AuthController;
class FreeDiskSpaceChecker;
class WebApplication; class WebApplication;
class WebSession final : public QObject, public ApplicationComponent, public ISession class WebSession final : public QObject, public ApplicationComponent, public ISession
@ -69,10 +73,12 @@ public:
void updateTimestamp(); void updateTimestamp();
template <typename T> template <typename T>
void registerAPIController(const QString &scope) T *registerAPIController(const QString &scope)
{ {
static_assert(std::is_base_of_v<APIController, T>, "Class should be derived from APIController."); static_assert(std::is_base_of_v<APIController, T>, "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; APIController *getAPIController(const QString &scope) const;
@ -97,15 +103,18 @@ public:
Http::Response processRequest(const Http::Request &request, const Http::Environment &env) override; Http::Response processRequest(const Http::Request &request, const Http::Environment &env) override;
const Http::Request &request() const;
const Http::Environment &env() const;
void setUsername(const QString &username);
void setPasswordHash(const QByteArray &passwordHash);
private:
QString clientId() const override; QString clientId() const override;
WebSession *session() override; WebSession *session() override;
void sessionStart() override; void sessionStart() override;
void sessionEnd() override; void sessionEnd() override;
const Http::Request &request() const;
const Http::Environment &env() const;
private:
void doProcessRequest(); void doProcessRequest();
void configure(); void configure();
@ -241,4 +250,8 @@ private:
QHostAddress m_clientAddress; QHostAddress m_clientAddress;
QVector<Http::Header> m_prebuiltHeaders; QVector<Http::Header> m_prebuiltHeaders;
Utils::Thread::UniquePtr m_workerThread;
FreeDiskSpaceChecker *m_freeDiskSpaceChecker = nullptr;
QTimer *m_freeDiskSpaceCheckingTimer = nullptr;
}; };

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015, 2023 Vladimir Golovnev <glassez@yandex.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -28,6 +28,7 @@
#include "webui.h" #include "webui.h"
#include "base/global.h"
#include "base/http/server.h" #include "base/http/server.h"
#include "base/logger.h" #include "base/logger.h"
#include "base/net/dnsupdater.h" #include "base/net/dnsupdater.h"
@ -36,10 +37,12 @@
#include "base/preferences.h" #include "base/preferences.h"
#include "base/utils/io.h" #include "base/utils/io.h"
#include "base/utils/net.h" #include "base/utils/net.h"
#include "base/utils/password.h"
#include "webapplication.h" #include "webapplication.h"
WebUI::WebUI(IApplication *app) WebUI::WebUI(IApplication *app, const QByteArray &tempPasswordHash)
: ApplicationComponent(app) : ApplicationComponent(app)
, m_passwordHash {tempPasswordHash}
{ {
configure(); configure();
connect(Preferences::instance(), &Preferences::changed, this, &WebUI::configure); connect(Preferences::instance(), &Preferences::changed, this, &WebUI::configure);
@ -49,12 +52,23 @@ void WebUI::configure()
{ {
m_isErrored = false; // clear previous error state m_isErrored = false; // clear previous error state
const QString portForwardingProfile = u"webui"_s;
const Preferences *pref = Preferences::instance(); 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 // Port forwarding
auto *portForwarder = Net::PortForwarder::instance(); auto *portForwarder = Net::PortForwarder::instance();
if (pref->useUPnPForWebUIPort()) if (pref->useUPnPForWebUIPort())
@ -67,7 +81,7 @@ void WebUI::configure()
} }
// http server // http server
const QString serverAddressString = pref->getWebUiAddress(); const QString serverAddressString = pref->getWebUIAddress();
const auto serverAddress = ((serverAddressString == u"*") || serverAddressString.isEmpty()) const auto serverAddress = ((serverAddressString == u"*") || serverAddressString.isEmpty())
? QHostAddress::Any : QHostAddress(serverAddressString); ? QHostAddress::Any : QHostAddress(serverAddressString);
@ -82,7 +96,10 @@ void WebUI::configure()
m_httpServer->close(); 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 const auto readData = [](const Path &path) -> QByteArray
{ {
@ -112,13 +129,8 @@ void WebUI::configure()
} }
else else
{ {
const QString errorMsg = tr("Web UI: Unable to bind to IP: %1, port: %2. Reason: %3") setError(tr("Unable to bind to IP: %1, port: %2. Reason: %3")
.arg(serverAddressString).arg(port).arg(m_httpServer->errorString()); .arg(serverAddressString).arg(port).arg(m_httpServer->errorString()));
LogMsg(errorMsg, Log::CRITICAL);
qCritical() << errorMsg;
m_isErrored = true;
emit fatalError();
} }
} }
@ -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 bool WebUI::isErrored() const
{ {
return m_isErrored; return m_isErrored;
} }
QString WebUI::errorMessage() const
{
return m_errorMsg;
}

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015, 2023 Vladimir Golovnev <glassez@yandex.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * 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) Q_DISABLE_COPY_MOVE(WebUI)
public: public:
explicit WebUI(IApplication *app); explicit WebUI(IApplication *app, const QByteArray &tempPasswordHash = {});
bool isErrored() const; bool isErrored() const;
QString errorMessage() const;
signals: signals:
void fatalError(); void fatalError();
@ -62,8 +63,13 @@ private slots:
void configure(); void configure();
private: private:
void setError(const QString &message);
bool m_isErrored = false; bool m_isErrored = false;
QString m_errorMsg;
QPointer<Http::Server> m_httpServer; QPointer<Http::Server> m_httpServer;
QPointer<Net::DNSUpdater> m_dnsUpdater; QPointer<Net::DNSUpdater> m_dnsUpdater;
QPointer<WebApplication> m_webapp; QPointer<WebApplication> m_webapp;
QByteArray m_passwordHash;
}; };

View file

@ -3,7 +3,6 @@ HEADERS += \
$$PWD/api/apierror.h \ $$PWD/api/apierror.h \
$$PWD/api/appcontroller.h \ $$PWD/api/appcontroller.h \
$$PWD/api/authcontroller.h \ $$PWD/api/authcontroller.h \
$$PWD/api/freediskspacechecker.h \
$$PWD/api/isessionmanager.h \ $$PWD/api/isessionmanager.h \
$$PWD/api/logcontroller.h \ $$PWD/api/logcontroller.h \
$$PWD/api/rsscontroller.h \ $$PWD/api/rsscontroller.h \
@ -12,6 +11,7 @@ HEADERS += \
$$PWD/api/torrentscontroller.h \ $$PWD/api/torrentscontroller.h \
$$PWD/api/transfercontroller.h \ $$PWD/api/transfercontroller.h \
$$PWD/api/serialize/serialize_torrent.h \ $$PWD/api/serialize/serialize_torrent.h \
$$PWD/freediskspacechecker.h \
$$PWD/webapplication.h \ $$PWD/webapplication.h \
$$PWD/webui.h $$PWD/webui.h
@ -20,7 +20,6 @@ SOURCES += \
$$PWD/api/apierror.cpp \ $$PWD/api/apierror.cpp \
$$PWD/api/appcontroller.cpp \ $$PWD/api/appcontroller.cpp \
$$PWD/api/authcontroller.cpp \ $$PWD/api/authcontroller.cpp \
$$PWD/api/freediskspacechecker.cpp \
$$PWD/api/logcontroller.cpp \ $$PWD/api/logcontroller.cpp \
$$PWD/api/rsscontroller.cpp \ $$PWD/api/rsscontroller.cpp \
$$PWD/api/searchcontroller.cpp \ $$PWD/api/searchcontroller.cpp \
@ -28,6 +27,7 @@ SOURCES += \
$$PWD/api/torrentscontroller.cpp \ $$PWD/api/torrentscontroller.cpp \
$$PWD/api/transfercontroller.cpp \ $$PWD/api/transfercontroller.cpp \
$$PWD/api/serialize/serialize_torrent.cpp \ $$PWD/api/serialize/serialize_torrent.cpp \
$$PWD/freediskspacechecker.cpp \
$$PWD/webapplication.cpp \ $$PWD/webapplication.cpp \
$$PWD/webui.cpp $$PWD/webui.cpp

View file

@ -178,8 +178,7 @@ a.propButton img {
} }
.scrollableMenu { .scrollableMenu {
overflow-x: hidden; overflow: hidden auto;
overflow-y: auto;
} }
/* context menu specific */ /* context menu specific */

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>QBT_TR(Renaming))QBT_TR[CONTEXT=TorrentContentTreeView]</title> <title>QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]</title>
<script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script>
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script src="scripts/filesystem.js?v=${CACHEID}"></script> <script src="scripts/filesystem.js?v=${CACHEID}"></script>

View file

@ -488,7 +488,21 @@ window.addEvent('load', function() {
Object.each(category_list, function(category) { Object.each(category_list, function(category) {
sortedCategories.push(category.name); 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) { for (let i = 0; i < sortedCategories.length; ++i) {
const categoryName = sortedCategories[i]; const categoryName = sortedCategories[i];

View file

@ -111,7 +111,8 @@ window.qBittorrent.DynamicTable = (function() {
let n = 2; 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; --n;
h -= 0.5; h -= 0.5;
$(this.dynamicTableDivId).style.height = h + 'px'; $(this.dynamicTableDivId).style.height = h + 'px';
@ -1001,10 +1002,13 @@ window.qBittorrent.DynamicTable = (function() {
case "checkingUP": case "checkingUP":
case "queuedForChecking": case "queuedForChecking":
case "checkingResumeData": case "checkingResumeData":
case "moving":
state = "force-recheck"; state = "force-recheck";
img_path = "images/force-recheck.svg"; img_path = "images/force-recheck.svg";
break; break;
case "moving":
state = "moving";
img_path = "images/set-location.svg";
break;
case "error": case "error":
case "unknown": case "unknown":
case "missingFiles": case "missingFiles":

View file

@ -167,7 +167,7 @@
window.parent.closeWindows(); window.parent.closeWindows();
}); });
if (Browser.platform === 'ios') { if ((Browser.platform === 'ios') || ((Browser.platform === 'mac') && (navigator.maxTouchPoints > 1))) {
$('fileselect').accept = ".torrent"; $('fileselect').accept = ".torrent";
} }
</script> </script>

View file

@ -349,6 +349,33 @@
</table> </table>
</fieldset> </fieldset>
<fieldset class="settings">
<legend>
<input type="checkbox" id="i2pEnabledCheckbox" onclick="qBittorrent.Preferences.updateI2PSettingsEnabled();" />
<label for="i2pEnabledCheckbox">QBT_TR(I2P (Experimental) (requires libtorrent &gt;= 2.0))QBT_TR[CONTEXT=OptionsDialog]</label>
</legend>
<table>
<tr>
<td>
<label for="i2pAddress">QBT_TR(Host:)QBT_TR[CONTEXT=OptionsDialog]</label>
</td>
<td>
<input type="text" id="i2pAddress" />
</td>
<td>
<label for="i2pPort">QBT_TR(Port:)QBT_TR[CONTEXT=OptionsDialog]</label>
</td>
<td>
<input type="number" id="i2pPort" min="0" max="65535" onchange="qBittorrent.Preferences.numberInputLimiter(this);" style="width: 5em;" />
</td>
</tr>
</table>
<div class="formRow">
<input type="checkbox" id="i2pMixedMode" title="QBT_TR(If &quot;mixed mode&quot; is enabled, I2P torrents are allowed to also get peers from other sources than the tracker, and connect to regular IPs, not providing any anonymization. This may be useful if the user is not interested in the anonymization of I2P, but still wants to be able to connect to I2P peers.)QBT_TR[CONTEXT=OptionsDialog]" />
<label for="i2pMixedMode">QBT_TR(Mixed mode)QBT_TR[CONTEXT=OptionsDialog]</label>
</div>
</fieldset>
<fieldset class="settings"> <fieldset class="settings">
<legend>QBT_TR(Proxy Server)QBT_TR[CONTEXT=OptionsDialog]</legend> <legend>QBT_TR(Proxy Server)QBT_TR[CONTEXT=OptionsDialog]</legend>
<table> <table>
@ -1420,6 +1447,38 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
<input type="text" id="requestQueueSize" style="width: 15em;" /> <input type="text" id="requestQueueSize" style="width: 15em;" />
</td> </td>
</tr> </tr>
<tr>
<td>
<label for="i2pInboundQuantity">QBT_TR(I2P inbound quantity (requires libtorrent &gt;= 2.0):)QBT_TR[CONTEXT=OptionsDialog]&nbsp;<a href="https://www.libtorrent.org/reference-Settings.html#i2p_inbound_quantity" target="_blank">(?)</a></label>
</td>
<td>
<input id="i2pInboundQuantity" type="number" min="1" max="16" onchange="qBittorrent.Preferences.numberInputLimiter(this);" style="width: 15em;" />
</td>
</tr>
<tr>
<td>
<label for="i2pOutboundQuantity">QBT_TR(I2P outbound quantity (requires libtorrent &gt;= 2.0):)QBT_TR[CONTEXT=OptionsDialog]&nbsp;<a href="https://www.libtorrent.org/reference-Settings.html#i2p_outbound_quantity" target="_blank">(?)</a></label>
</td>
<td>
<input id="i2pOutboundQuantity" type="number" min="1" max="16" onchange="qBittorrent.Preferences.numberInputLimiter(this);" style="width: 15em;" />
</td>
</tr>
<tr>
<td>
<label for="i2pInboundLength">QBT_TR(I2P inbound length (requires libtorrent &gt;= 2.0):)QBT_TR[CONTEXT=OptionsDialog]&nbsp;<a href="https://www.libtorrent.org/reference-Settings.html#i2p_inbound_length" target="_blank">(?)</a></label>
</td>
<td>
<input id="i2pInboundLength" type="number" min="0" max="7" onchange="qBittorrent.Preferences.numberInputLimiter(this);" style="width: 15em;" />
</td>
</tr>
<tr>
<td>
<label for="i2pOutboundLength">QBT_TR(I2P outbound length (requires libtorrent &gt;= 2.0):)QBT_TR[CONTEXT=OptionsDialog]&nbsp;<a href="https://www.libtorrent.org/reference-Settings.html#i2p_outbound_length" target="_blank">(?)</a></label>
</td>
<td>
<input id="i2pOutboundLength" type="number" min="0" max="7" onchange="qBittorrent.Preferences.numberInputLimiter(this);" style="width: 15em;" />
</td>
</tr>
</table> </table>
</fieldset> </fieldset>
</div> </div>
@ -1455,6 +1514,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
updateMaxConnecPerTorrentEnabled: updateMaxConnecPerTorrentEnabled, updateMaxConnecPerTorrentEnabled: updateMaxConnecPerTorrentEnabled,
updateMaxUploadsEnabled: updateMaxUploadsEnabled, updateMaxUploadsEnabled: updateMaxUploadsEnabled,
updateMaxUploadsPerTorrentEnabled: updateMaxUploadsPerTorrentEnabled, updateMaxUploadsPerTorrentEnabled: updateMaxUploadsPerTorrentEnabled,
updateI2PSettingsEnabled: updateI2PSettingsEnabled,
updatePeerProxySettings: updatePeerProxySettings, updatePeerProxySettings: updatePeerProxySettings,
updatePeerProxyAuthSettings: updatePeerProxyAuthSettings, updatePeerProxyAuthSettings: updatePeerProxyAuthSettings,
updateFilterSettings: updateFilterSettings, 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); $('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 updatePeerProxySettings = function() {
const proxyType = $('peer_proxy_type_select').getProperty('value'); const proxyType = $('peer_proxy_type_select').getProperty('value');
const isProxyDisabled = (proxyType === "None"); const isProxyDisabled = (proxyType === "None");
@ -2023,6 +2090,13 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
} }
updateMaxUploadsPerTorrentEnabled(); 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 // Proxy Server
$('peer_proxy_type_select').setProperty('value', pref.proxy_type); $('peer_proxy_type_select').setProperty('value', pref.proxy_type);
$('peer_proxy_host_text').setProperty('value', pref.proxy_ip); $('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); $('peerTurnoverCutoff').setProperty('value', pref.peer_turnover_cutoff);
$('peerTurnoverInterval').setProperty('value', pref.peer_turnover_interval); $('peerTurnoverInterval').setProperty('value', pref.peer_turnover_interval);
$('requestQueueSize').setProperty('value', pref.request_queue_size); $('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(); }).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); 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 // Proxy Server
settings.set('proxy_type', $('peer_proxy_type_select').getProperty('value')); settings.set('proxy_type', $('peer_proxy_type_select').getProperty('value'));
settings.set('proxy_ip', $('peer_proxy_host_text').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_cutoff', $('peerTurnoverCutoff').getProperty('value'));
settings.set('peer_turnover_interval', $('peerTurnoverInterval').getProperty('value')); settings.set('peer_turnover_interval', $('peerTurnoverInterval').getProperty('value'));
settings.set('request_queue_size', $('requestQueueSize').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 // Send it to qBT
const json_str = JSON.encode(settings); const json_str = JSON.encode(settings);

View file

@ -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.category = $('assignCategoryCombobox').value;
rulesList[rule].torrentParams.tags = $('ruleAddTags').value.split(','); 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) { switch ($('addPausedCombobox').value) {
case 'default': case 'default':
@ -715,8 +718,6 @@ Supports the formats: S01E01, 1x1, 2017.12.31 and 31.12.2017 (Date formats also
$('assignCategoryCombobox').disabled = false; $('assignCategoryCombobox').disabled = false;
$('ruleAddTags').disabled = false; $('ruleAddTags').disabled = false;
$('savetoDifferentDir').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; $('ignoreDaysValue').disabled = false;
$('addPausedCombobox').disabled = false; $('addPausedCombobox').disabled = false;
$('contentLayoutCombobox').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'; $('assignCategoryCombobox').value = rulesList[ruleName].torrentParams.category ? rulesList[ruleName].torrentParams.category : 'default';
$('ruleAddTags').value = rulesList[ruleName].torrentParams.tags.join(','); $('ruleAddTags').value = rulesList[ruleName].torrentParams.tags.join(',');
$('savetoDifferentDir').checked = rulesList[ruleName].torrentParams.save_path !== ''; $('savetoDifferentDir').checked = rulesList[ruleName].torrentParams.save_path !== '';
$('saveToText').disabled = !$('savetoDifferentDir').checked;
$('saveToText').value = rulesList[ruleName].torrentParams.save_path; $('saveToText').value = rulesList[ruleName].torrentParams.save_path;
$('ignoreDaysValue').value = rulesList[ruleName].ignoreDays; $('ignoreDaysValue').value = rulesList[ruleName].ignoreDays;

View file

@ -23,14 +23,14 @@
<img src="images/qbittorrent-tray.svg" alt="qBittorrent logo" /> <img src="images/qbittorrent-tray.svg" alt="qBittorrent logo" />
</div> </div>
<div id="formplace" class="col"> <div id="formplace" class="col">
<form id="loginform" method="post" onsubmit="submitLoginForm();"> <form id="loginform" method="post" onsubmit="submitLoginForm(event);">
<div class="row"> <div class="row">
<label for="username">QBT_TR(Username)QBT_TR[CONTEXT=HttpServer]</label><br /> <label for="username">QBT_TR(Username)QBT_TR[CONTEXT=HttpServer]</label><br />
<input type="text" id="username" name="username" autocomplete="username" /> <input type="text" id="username" name="username" autocomplete="username" required />
</div> </div>
<div class="row"> <div class="row">
<label for="password">QBT_TR(Password)QBT_TR[CONTEXT=HttpServer]</label><br /> <label for="password">QBT_TR(Password)QBT_TR[CONTEXT=HttpServer]</label><br />
<input type="password" id="password" name="password" autocomplete="current-password" /> <input type="password" id="password" name="password" autocomplete="current-password" required />
</div> </div>
<div class="row"> <div class="row">
<input type="submit" id="login" value="QBT_TR(Login)QBT_TR[CONTEXT=HttpServer]" /> <input type="submit" id="login" value="QBT_TR(Login)QBT_TR[CONTEXT=HttpServer]" />

View file

@ -31,13 +31,10 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
document.getElementById('username').focus(); document.getElementById('username').focus();
document.getElementById('username').select(); 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 errorMsgElement = document.getElementById('error_msg');
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();