diff --git a/WebAPI_Changelog.md b/WebAPI_Changelog.md index 7be0c987a..19dafdedc 100644 --- a/WebAPI_Changelog.md +++ b/WebAPI_Changelog.md @@ -1,5 +1,13 @@ # WebAPI Changelog +## 2.14.0 +* [#23202](https://github.com/qbittorrent/qBittorrent/pull/23202) + * WebAPI responds with the error message "Endpoint does not exist" when the endpoint does not exist, to better differentiate from unrelated Not Found (i.e. 404) responses + * `auth/login` endpoint responds to invalid credentials with a 401 + * `torrents/add` endpoint responds with `success_count`, `pending_count`, `failure_count`, and `added_torrent_ids` + * When `pending_count` is non-zero, response code 202 is used + * When all torrents fail to be added, response code 409 is used + ## 2.13.1 * [#23163](https://github.com/qbittorrent/qBittorrent/pull/23163) * `torrents/add` endpoint now supports downloading from a search plugin via the `downloader` parameter diff --git a/src/webui/api/apicontroller.cpp b/src/webui/api/apicontroller.cpp index 494bff280..e1b9d52b5 100644 --- a/src/webui/api/apicontroller.cpp +++ b/src/webui/api/apicontroller.cpp @@ -59,7 +59,7 @@ APIResult APIController::run(const QString &action, const StringMap ¶ms, con const QByteArray methodName = action.toLatin1() + "Action"; if (!QMetaObject::invokeMethod(this, methodName.constData())) - throw APIError(APIErrorType::NotFound); + throw APIError(APIErrorType::NotFound, tr("Endpoint does not exist")); return m_result; } diff --git a/src/webui/api/apierror.h b/src/webui/api/apierror.h index 3879bd3af..b0a87828f 100644 --- a/src/webui/api/apierror.h +++ b/src/webui/api/apierror.h @@ -32,11 +32,12 @@ enum class APIErrorType { + AccessDenied, BadParams, BadData, + Conflict, NotFound, - AccessDenied, - Conflict + Unauthorized }; class APIError : public RuntimeError diff --git a/src/webui/api/authcontroller.cpp b/src/webui/api/authcontroller.cpp index ee52c1df6..c2db0d596 100644 --- a/src/webui/api/authcontroller.cpp +++ b/src/webui/api/authcontroller.cpp @@ -59,7 +59,7 @@ void AuthController::loginAction() { if (m_sessionManager->session()) { - setResult(u"Ok."_s); + setStatus(APIStatus::Ok); return; } @@ -84,17 +84,17 @@ void AuthController::loginAction() m_clientFailedLogins.remove(clientAddr); m_sessionManager->sessionStart(); - setResult(u"Ok."_s); + setStatus(APIStatus::Ok); LogMsg(tr("WebAPI login success. IP: %1").arg(clientAddr)); } else { if (Preferences::instance()->getWebUIMaxAuthFailCount() > 0) increaseFailedAttempts(); - setResult(u"Fails."_s); LogMsg(tr("WebAPI login failure. Reason: invalid credentials, attempt count: %1, IP: %2, username: %3") .arg(QString::number(failedAttemptsCount()), clientAddr, usernameFromWeb) , Log::WARNING); + throw APIError(APIErrorType::Unauthorized); } } diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 76e1ca29b..9eeb183bc 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -1098,7 +1098,10 @@ void TorrentsController::addAction() }; - bool partialSuccess = false; + int pending = 0; + int failure = 0; + QList addedTorrentIDs; + addedTorrentIDs.reserve(urls.size() + torrents.size()); for (QString url : urls) { url = url.trimmed(); @@ -1123,7 +1126,14 @@ void TorrentsController::addAction() addTorrentParams.filePriorities = filePriorities; } - partialSuccess |= BitTorrent::Session::instance()->addTorrent(torrentDescr, addTorrentParams); + if (BitTorrent::Session::instance()->addTorrent(torrentDescr, addTorrentParams)) + { + addedTorrentIDs.append(torrentID); + } + else + { + ++failure; + } } else if (!downloaderParam.isEmpty()) { @@ -1137,14 +1147,29 @@ void TorrentsController::addAction() app()->addTorrentManager()->addTorrent(torrentFilePath, addTorrentParams); }); connect(downloadHandler, &SearchDownloadHandler::downloadFinished, downloadHandler, &SearchDownloadHandler::deleteLater); - partialSuccess = true; + + ++pending; } else { if (!filePriorities.isEmpty()) throw APIError(APIErrorType::BadParams, tr("`filePriorities` may only be specified when metadata has already been fetched")); - partialSuccess |= app()->addTorrentManager()->addTorrent(url, addTorrentParams); + if (app()->addTorrentManager()->addTorrent(url, addTorrentParams)) + { + if (infoHash.isValid()) + { + addedTorrentIDs.append(torrentID); + } + else + { + ++pending; + } + } + else + { + ++failure; + } } m_torrentSourceCache.remove(url); m_torrentMetadataCache.remove(torrentID); @@ -1155,7 +1180,15 @@ void TorrentsController::addAction() { if (const auto loadResult = BitTorrent::TorrentDescriptor::load(it.value())) { - partialSuccess |= BitTorrent::Session::instance()->addTorrent(loadResult.value(), addTorrentParams); + const BitTorrent::TorrentDescriptor &torrentDescr = loadResult.value(); + if (BitTorrent::Session::instance()->addTorrent(torrentDescr, addTorrentParams)) + { + addedTorrentIDs.append(torrentDescr.infoHash().toTorrentID()); + } + else + { + ++failure; + } } else { @@ -1163,10 +1196,25 @@ void TorrentsController::addAction() } } - if (partialSuccess) - setResult(u"Ok."_s); + if (!addedTorrentIDs.isEmpty() || (pending > 0)) + { + QJsonArray ids; + for (const BitTorrent::TorrentID &torrentID : addedTorrentIDs) + ids.append(torrentID.toString()); + + setResult(QJsonObject { + {u"success_count"_s, addedTorrentIDs.size()}, + {u"failure_count"_s, failure}, + {u"pending_count"_s, pending}, + {u"added_torrent_ids"_s, ids}, + }); + if (pending > 0) + setStatus(APIStatus::Async); + } else - setResult(u"Fails."_s); + { + throw APIError(APIErrorType::Conflict); + } } void TorrentsController::addTrackersAction() diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index b771a863c..d398f6559 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -418,6 +418,8 @@ void WebApplication::doProcessRequest() throw ConflictHTTPError(error.message()); case APIErrorType::NotFound: throw NotFoundHTTPError(error.message()); + case APIErrorType::Unauthorized: + throw UnauthorizedHTTPError(error.message()); default: Q_UNREACHABLE(); break; diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 1e6a89076..41201caa9 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -53,7 +53,7 @@ #include "base/utils/version.h" #include "api/isessionmanager.h" -inline const Utils::Version<3, 2> API_VERSION {2, 13, 1}; +inline const Utils::Version<3, 2> API_VERSION {2, 14, 0}; class APIController; class AuthController; diff --git a/src/webui/www/public/scripts/login.js b/src/webui/www/public/scripts/login.js index cbe45e606..149e405ae 100644 --- a/src/webui/www/public/scripts/login.js +++ b/src/webui/www/public/scripts/login.js @@ -45,12 +45,12 @@ const submitLoginForm = (event) => { }) }) .then(async (response) => { - const responseText = await response.text(); - if (response.ok && (responseText === "Ok.")) { + if (response.ok) { location.replace(location); // redirect location.reload(true); } else { + const responseText = await response.text(); errorMsgElement.textContent = `QBT_TR(Invalid Username or Password.)QBT_TR[CONTEXT=Login]\nQBT_TR(Server response:)QBT_TR[CONTEXT=Login] ${responseText}`; } },