diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index d5cd8d1e495e..66e2fe478ab9 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -135,6 +135,8 @@ namespace using Utils::String::parseInt; using Utils::String::parseDouble; + const QStringList SUPPORTED_WEBSEED_SCHEMES{u"http"_s, u"https"_s, u"ftp"_s}; + void applyToTorrents(const QStringList &idList, const std::function &func) { if ((idList.size() == 1) && (idList[0] == u"all")) @@ -252,6 +254,25 @@ namespace idList << BitTorrent::TorrentID::fromString(hash); return idList; } + + nonstd::expected validateWebseedUrl(const QString &urlStr) + { + const QString normalizedUrlStr = QUrl::fromPercentEncoding(urlStr.toUtf8()); + + // sanity check before parsing to prevent QUrl from checking for the existence of files on the filesystem + if (!SUPPORTED_WEBSEED_SCHEMES.contains(normalizedUrlStr.section(u"://"_s, 0, 0))) + return nonstd::make_unexpected(TorrentsController::tr("URL scheme must be one of [%1]").arg(SUPPORTED_WEBSEED_SCHEMES.join(u","))); + + const QUrl url {normalizedUrlStr, QUrl::StrictMode}; + if (!url.isValid()) + return nonstd::make_unexpected(TorrentsController::tr("\"%1\" is not a valid url").arg(normalizedUrlStr)); + + // sanity check after parsing in case QUrl interpreted a different scheme than intended + if (!SUPPORTED_WEBSEED_SCHEMES.contains(url.scheme())) + return nonstd::make_unexpected(TorrentsController::tr("URL scheme must be one of [%1]").arg(SUPPORTED_WEBSEED_SCHEMES.join(u","))); + + return url; + } } void TorrentsController::countAction() @@ -560,6 +581,84 @@ void TorrentsController::webseedsAction() setResult(webSeedList); } +void TorrentsController::addWebseedsAction() +{ + requireParams({u"hash"_s, u"urls"_s}); + const QStringList paramUrls = params()[u"urls"_s].split(u'|', Qt::SkipEmptyParts); + + const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); + BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + QList urls; + urls.reserve(paramUrls.size()); + for (const QString &urlStr : paramUrls) + { + const auto result = validateWebseedUrl(urlStr); + if (!result) + throw APIError(APIErrorType::BadParams, result.error()); + urls << result.value(); + } + + torrent->addUrlSeeds(urls); +} + +void TorrentsController::editWebseedAction() +{ + requireParams({u"hash"_s, u"origUrl"_s, u"newUrl"_s}); + + const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); + const QString origUrlStr = params()[u"origUrl"_s]; + const QString newUrlStr = params()[u"newUrl"_s]; + + BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + const auto origUrlResult = validateWebseedUrl(origUrlStr); + if (!origUrlResult) + throw APIError(APIErrorType::BadParams, origUrlResult.error()); + const QUrl origUrl = origUrlResult.value(); + + const auto newUrlResult = validateWebseedUrl(newUrlStr); + if (!newUrlResult) + throw APIError(APIErrorType::BadParams, newUrlResult.error()); + const QUrl newUrl = newUrlResult.value(); + + if (newUrl != origUrl) + { + if (!torrent->urlSeeds().contains(origUrl)) + throw APIError(APIErrorType::BadParams, tr("\"%1\" is not an existing url").arg(origUrl.toString())); + + torrent->removeUrlSeeds({origUrl}); + torrent->addUrlSeeds({newUrl}); + } +} + +void TorrentsController::removeWebseedsAction() +{ + requireParams({u"hash"_s, u"urls"_s}); + const QStringList paramUrls = params()[u"urls"_s].split(u'|', Qt::SkipEmptyParts); + + const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); + BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + QList urls; + urls.reserve(paramUrls.size()); + for (const QString &urlStr : paramUrls) + { + const auto result = validateWebseedUrl(urlStr); + if (!result) + throw APIError(APIErrorType::BadParams, result.error()); + urls << result.value(); + } + + torrent->removeUrlSeeds(urls); +} + // Returns the files in a torrent in JSON format. // The return value is a JSON-formatted list of dictionaries. // The dictionary keys are: diff --git a/src/webui/api/torrentscontroller.h b/src/webui/api/torrentscontroller.h index d731b0de8f5c..c6605f3d3ad6 100644 --- a/src/webui/api/torrentscontroller.h +++ b/src/webui/api/torrentscontroller.h @@ -44,6 +44,9 @@ private slots: void propertiesAction(); void trackersAction(); void webseedsAction(); + void addWebseedsAction(); + void editWebseedAction(); + void removeWebseedsAction(); void filesAction(); void pieceHashesAction(); void pieceStatesAction(); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 4d304934100b..f0082c8d9254 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -177,6 +177,7 @@ class WebApplication final : public ApplicationComponent {{u"torrents"_s, u"addPeers"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"addTags"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"addTrackers"_s}, Http::METHOD_POST}, + {{u"torrents"_s, u"addWebseeds"_s}, Http::METHOD_POST}, {{u"transfer"_s, u"banPeers"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"bottomPrio"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"createCategory"_s}, Http::METHOD_POST}, @@ -186,6 +187,7 @@ class WebApplication final : public ApplicationComponent {{u"torrents"_s, u"deleteTags"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"editCategory"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"editTracker"_s}, Http::METHOD_POST}, + {{u"torrents"_s, u"editWebseed"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"filePrio"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"increasePrio"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"reannounce"_s}, Http::METHOD_POST}, @@ -193,6 +195,7 @@ class WebApplication final : public ApplicationComponent {{u"torrents"_s, u"removeCategories"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"removeTags"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"removeTrackers"_s}, Http::METHOD_POST}, + {{u"torrents"_s, u"removeWebseeds"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"rename"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"renameFile"_s}, Http::METHOD_POST}, {{u"torrents"_s, u"renameFolder"_s}, Http::METHOD_POST},