mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2026-01-08 16:42:30 -06:00
Compare commits
4 Commits
feacfb0627
...
4be33b2ddc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4be33b2ddc | ||
|
|
6830e32c72 | ||
|
|
2f34c9b2f0 | ||
|
|
bda37cbade |
@@ -10,6 +10,8 @@
|
||||
* `torrents/editTracker` endpoint now supports setting a tracker's tier via `tier` parameter
|
||||
* `torrents/editTracker` endpoint always responds with a 204 when successful
|
||||
* `torrents/editTracker` endpoint `origUrl` parameter renamed to `url`
|
||||
* [#23085](https://github.com/qbittorrent/qBittorrent/pull/23085)
|
||||
* `torrents/parseMetadata` now responds with an array of metadata in the same order as the files in the request. It previously responded with an object keyed off of the submitted file name.
|
||||
|
||||
## 2.12.1
|
||||
* [#23031](https://github.com/qbittorrent/qBittorrent/pull/23031)
|
||||
|
||||
@@ -31,12 +31,13 @@
|
||||
#include "requestparser.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include <QByteArrayList>
|
||||
#include <QByteArrayView>
|
||||
#include <QDebug>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
|
||||
@@ -59,21 +60,19 @@ namespace
|
||||
return in;
|
||||
}
|
||||
|
||||
bool parseHeaderLine(const QStringView line, HeaderMap &out)
|
||||
std::optional<QStringPair> parseHeaderLine(const QByteArrayView line)
|
||||
{
|
||||
// [rfc7230] 3.2. Header Fields
|
||||
const int i = line.indexOf(u':');
|
||||
if (i <= 0)
|
||||
{
|
||||
qWarning() << Q_FUNC_INFO << "invalid http header:" << line;
|
||||
return false;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QString name = line.first(i).trimmed().toString().toLower();
|
||||
const QString value = line.sliced(i + 1).trimmed().toString();
|
||||
out[name] = value;
|
||||
|
||||
return true;
|
||||
const QString name = QString::fromLatin1(line.first(i).trimmed()).toLower();
|
||||
const QString value = QString::fromLatin1(line.sliced(i + 1).trimmed());
|
||||
return {{name, value}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +92,7 @@ RequestParser::ParseResult RequestParser::doParse(const QByteArrayView data)
|
||||
return {ParseStatus::Incomplete, Request(), 0};
|
||||
}
|
||||
|
||||
const QString httpHeaders = QString::fromLatin1(data.constData(), headerEnd);
|
||||
const QByteArrayView httpHeaders = data.first(headerEnd);
|
||||
if (!parseStartLines(httpHeaders))
|
||||
{
|
||||
qWarning() << Q_FUNC_INFO << "header parsing error";
|
||||
@@ -152,36 +151,40 @@ RequestParser::ParseResult RequestParser::doParse(const QByteArrayView data)
|
||||
return {ParseStatus::BadMethod, m_request, 0};
|
||||
}
|
||||
|
||||
bool RequestParser::parseStartLines(const QStringView data)
|
||||
bool RequestParser::parseStartLines(const QByteArrayView data)
|
||||
{
|
||||
// we don't handle malformed request which uses `LF` for newline
|
||||
const QList<QStringView> lines = data.split(QString::fromLatin1(CRLF), Qt::SkipEmptyParts);
|
||||
const QList<QByteArrayView> lines = splitToViews(data, CRLF, Qt::SkipEmptyParts);
|
||||
|
||||
// [rfc7230] 3.2.2. Field Order
|
||||
QStringList requestLines;
|
||||
QByteArrayList requestLines;
|
||||
for (const auto &line : lines)
|
||||
{
|
||||
if (line.at(0).isSpace() && !requestLines.isEmpty())
|
||||
if (QChar::fromLatin1(line.at(0)).isSpace() && !requestLines.isEmpty())
|
||||
{
|
||||
// continuation of previous line
|
||||
requestLines.last() += line;
|
||||
}
|
||||
else
|
||||
{
|
||||
requestLines += line.toString();
|
||||
requestLines += line.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
if (requestLines.isEmpty())
|
||||
return false;
|
||||
|
||||
if (!parseRequestLine(requestLines[0]))
|
||||
if (!parseRequestLine(QString::fromLatin1(requestLines[0])))
|
||||
return false;
|
||||
|
||||
for (auto i = ++(requestLines.begin()); i != requestLines.end(); ++i)
|
||||
{
|
||||
if (!parseHeaderLine(*i, m_request.headers))
|
||||
const std::optional<QStringPair> header = parseHeaderLine(*i);
|
||||
if (!header.has_value())
|
||||
return false;
|
||||
|
||||
const auto [name, value] = header.value();
|
||||
m_request.headers[name] = value;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -310,17 +313,23 @@ bool RequestParser::parseFormData(const QByteArrayView data)
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString headers = QString::fromLatin1(data.first(eohPos));
|
||||
const QByteArrayView headers = data.first(eohPos);
|
||||
const QByteArrayView payload = viewWithoutEndingWith(data.sliced((eohPos + EOH.size())), CRLF);
|
||||
|
||||
HeaderMap headersMap;
|
||||
const QList<QStringView> headerLines = QStringView(headers).split(QString::fromLatin1(CRLF), Qt::SkipEmptyParts);
|
||||
const QList<QByteArrayView> headerLines = splitToViews(headers, CRLF, Qt::SkipEmptyParts);
|
||||
for (const auto &line : headerLines)
|
||||
{
|
||||
if (line.trimmed().startsWith(HEADER_CONTENT_DISPOSITION, Qt::CaseInsensitive))
|
||||
const std::optional<QStringPair> header = parseHeaderLine(line);
|
||||
if (!header.has_value())
|
||||
return false;
|
||||
|
||||
const auto [name, value] = header.value();
|
||||
|
||||
if (name == HEADER_CONTENT_DISPOSITION)
|
||||
{
|
||||
// extract out filename & name
|
||||
const QList<QStringView> directives = line.split(u';', Qt::SkipEmptyParts);
|
||||
const QList<QByteArrayView> directives = splitToViews(line, ";", Qt::SkipEmptyParts);
|
||||
|
||||
for (const auto &directive : directives)
|
||||
{
|
||||
@@ -328,15 +337,14 @@ bool RequestParser::parseFormData(const QByteArrayView data)
|
||||
if (idx < 0)
|
||||
continue;
|
||||
|
||||
const QString name = directive.first(idx).trimmed().toString().toLower();
|
||||
const QString value = Utils::String::unquote(directive.sliced(idx + 1).trimmed()).toString();
|
||||
const QString name = QString::fromLatin1(directive.first(idx).trimmed()).toLower();
|
||||
const QString value = QString::fromLatin1(unquote(directive.sliced(idx + 1).trimmed()));
|
||||
headersMap[name] = value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!parseHeaderLine(line, headersMap))
|
||||
return false;
|
||||
headersMap[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace Http
|
||||
RequestParser() = default;
|
||||
|
||||
ParseResult doParse(QByteArrayView data);
|
||||
bool parseStartLines(QStringView data);
|
||||
bool parseStartLines(QByteArrayView data);
|
||||
bool parseRequestLine(const QString &line);
|
||||
|
||||
bool parsePostMessage(QByteArrayView data);
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Http
|
||||
inline const QString HEADER_X_CONTENT_TYPE_OPTIONS = u"x-content-type-options"_s;
|
||||
inline const QString HEADER_X_FORWARDED_FOR = u"x-forwarded-for"_s;
|
||||
inline const QString HEADER_X_FORWARDED_HOST = u"x-forwarded-host"_s;
|
||||
inline const QString HEADER_X_FORWARDED_PROTO = u"X-forwarded-proto"_s;
|
||||
inline const QString HEADER_X_FORWARDED_PROTO = u"x-forwarded-proto"_s;
|
||||
inline const QString HEADER_X_FRAME_OPTIONS = u"x-frame-options"_s;
|
||||
inline const QString HEADER_X_XSS_PROTECTION = u"x-xss-protection"_s;
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QByteArrayMatcher>
|
||||
#include <QByteArrayView>
|
||||
#include <QList>
|
||||
|
||||
QList<QByteArrayView> Utils::ByteArray::splitToViews(const QByteArrayView in, const QByteArrayView sep, const Qt::SplitBehavior behavior)
|
||||
|
||||
@@ -31,9 +31,9 @@
|
||||
|
||||
#include <Qt>
|
||||
#include <QtContainerFwd>
|
||||
#include <QByteArrayView>
|
||||
|
||||
class QByteArray;
|
||||
class QByteArrayView;
|
||||
|
||||
namespace Utils::ByteArray
|
||||
{
|
||||
@@ -42,4 +42,19 @@ namespace Utils::ByteArray
|
||||
QByteArray asQByteArray(QByteArrayView view);
|
||||
|
||||
QByteArray toBase32(const QByteArray &in);
|
||||
|
||||
template <typename T>
|
||||
T unquote(const T &arr, const QByteArrayView quotes = "\"")
|
||||
{
|
||||
if (arr.length() < 2)
|
||||
return arr;
|
||||
|
||||
for (const char quote : quotes)
|
||||
{
|
||||
if (arr.startsWith(quote) && arr.endsWith(quote))
|
||||
return arr.sliced(1, (arr.length() - 2));
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2099,16 +2099,14 @@ void TorrentsController::parseMetadataAction()
|
||||
if (uploadedTorrents.isEmpty())
|
||||
throw APIError(APIErrorType::BadParams, tr("Must specify torrent file(s)"));
|
||||
|
||||
QJsonObject result;
|
||||
QJsonArray result;
|
||||
for (auto it = uploadedTorrents.constBegin(); it != uploadedTorrents.constEnd(); ++it)
|
||||
{
|
||||
if (const auto loadResult = BitTorrent::TorrentDescriptor::load(it.value()))
|
||||
{
|
||||
const BitTorrent::TorrentDescriptor &torrentDescr = loadResult.value();
|
||||
m_torrentMetadataCache.insert(torrentDescr.infoHash().toTorrentID(), torrentDescr);
|
||||
|
||||
const QString &fileName = it.key();
|
||||
result.insert(fileName, serializeTorrentInfo(torrentDescr));
|
||||
result.append(serializeTorrentInfo(torrentDescr));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -76,77 +76,52 @@ window.qBittorrent.Cache ??= (() => {
|
||||
class PreferencesCache {
|
||||
#m_store = {};
|
||||
|
||||
// obj: {
|
||||
// onFailure: () => {},
|
||||
// onSuccess: () => {}
|
||||
// }
|
||||
init(obj = {}) {
|
||||
return fetch("api/v2/app/preferences", {
|
||||
async init() {
|
||||
return await fetch("api/v2/app/preferences", {
|
||||
method: "GET",
|
||||
cache: "no-store"
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok)
|
||||
return;
|
||||
if (!response.ok)
|
||||
return;
|
||||
|
||||
const responseText = await response.text();
|
||||
const responseJSON = JSON.parse(responseText);
|
||||
deepFreeze(responseJSON);
|
||||
this.#m_store = responseJSON;
|
||||
const responseText = await response.text();
|
||||
const responseJSON = JSON.parse(responseText);
|
||||
deepFreeze(responseJSON);
|
||||
this.#m_store = responseJSON;
|
||||
|
||||
if (typeof obj.onSuccess === "function")
|
||||
obj.onSuccess(responseJSON, responseText);
|
||||
},
|
||||
(error) => {
|
||||
if (typeof obj.onFailure === "function")
|
||||
obj.onFailure(error);
|
||||
});
|
||||
return responseJSON;
|
||||
});
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.#m_store;
|
||||
}
|
||||
|
||||
// obj: {
|
||||
// data: {},
|
||||
// onFailure: () => {},
|
||||
// onSuccess: () => {}
|
||||
// }
|
||||
set(obj) {
|
||||
if (typeof obj !== "object")
|
||||
throw new Error("`obj` is not an object.");
|
||||
if (typeof obj.data !== "object")
|
||||
async set(data) {
|
||||
if (typeof data !== "object")
|
||||
throw new Error("`data` is not an object.");
|
||||
|
||||
fetch("api/v2/app/setPreferences", {
|
||||
return await fetch("api/v2/app/setPreferences", {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
json: JSON.stringify(obj.data)
|
||||
json: JSON.stringify(data)
|
||||
})
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok)
|
||||
return;
|
||||
.then((response) => {
|
||||
if (!response.ok)
|
||||
return;
|
||||
|
||||
this.#m_store = structuredClone(this.#m_store);
|
||||
for (const key in obj.data) {
|
||||
if (!Object.hasOwn(obj.data, key))
|
||||
continue;
|
||||
this.#m_store = structuredClone(this.#m_store);
|
||||
for (const key in data) {
|
||||
if (!Object.hasOwn(data, key))
|
||||
continue;
|
||||
|
||||
const value = obj.data[key];
|
||||
this.#m_store[key] = value;
|
||||
}
|
||||
deepFreeze(this.#m_store);
|
||||
|
||||
if (typeof obj.onSuccess === "function") {
|
||||
const responseText = await response.text();
|
||||
obj.onSuccess(responseText);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (typeof obj.onFailure === "function")
|
||||
obj.onFailure(error);
|
||||
});
|
||||
const value = data[key];
|
||||
this.#m_store[key] = value;
|
||||
}
|
||||
deepFreeze(this.#m_store);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -175,9 +175,11 @@ window.qBittorrent.Client ??= (() => {
|
||||
const uploadTorrentFiles = (files) => {
|
||||
const fileNames = [];
|
||||
const formData = new FormData();
|
||||
for (const file of files) {
|
||||
for (let i = 0; i < files.length; ++i) {
|
||||
const file = files[i];
|
||||
fileNames.push(file.name);
|
||||
formData.append("file", file);
|
||||
// send dummy file name as file name won't be used and may not be encoded properly
|
||||
formData.append("file", file, i);
|
||||
}
|
||||
|
||||
fetch("api/v2/torrents/parseMetadata", {
|
||||
@@ -191,12 +193,9 @@ window.qBittorrent.Client ??= (() => {
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
for (const fileName of fileNames) {
|
||||
let title = fileName;
|
||||
const metadata = json[fileName];
|
||||
if (metadata !== undefined)
|
||||
title = metadata.name;
|
||||
|
||||
for (let i = 0; i < json.length; ++i) {
|
||||
const metadata = json[i];
|
||||
const title = metadata.name || fileNames[i];
|
||||
const hash = metadata.infohash_v2 || metadata.infohash_v1;
|
||||
createAddTorrentWindow(title, hash, metadata);
|
||||
}
|
||||
|
||||
@@ -58,13 +58,10 @@
|
||||
// Set current "Delete files" choice as the default
|
||||
rememberButton.addEventListener("click", (e) => {
|
||||
window.qBittorrent.Cache.preferences.set({
|
||||
data: {
|
||||
delete_torrent_content_files: deleteCB.checked
|
||||
},
|
||||
onSuccess: () => {
|
||||
prefDeleteContentFiles = deleteCB.checked;
|
||||
setRememberBtnEnabled(false);
|
||||
}
|
||||
delete_torrent_content_files: deleteCB.checked
|
||||
}).then(() => {
|
||||
prefDeleteContentFiles = deleteCB.checked;
|
||||
setRememberBtnEnabled(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2228,8 +2228,8 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
|
||||
};
|
||||
|
||||
const loadPreferences = () => {
|
||||
window.parent.qBittorrent.Cache.preferences.init({
|
||||
onSuccess: (pref) => {
|
||||
window.parent.qBittorrent.Cache.preferences.init()
|
||||
.then((pref) => {
|
||||
// Behavior tab
|
||||
// Language
|
||||
updateWebuiLocaleSelect(pref.locale);
|
||||
@@ -2650,8 +2650,10 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
|
||||
document.getElementById("i2pOutboundQuantity").value = pref.i2p_outbound_quantity;
|
||||
document.getElementById("i2pInboundLength").value = pref.i2p_inbound_length;
|
||||
document.getElementById("i2pOutboundLength").value = pref.i2p_outbound_length;
|
||||
}
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
alert("QBT_TR(Unable to load program preferences, qBittorrent is probably unreachable.)QBT_TR[CONTEXT=HttpServer]");
|
||||
});
|
||||
};
|
||||
|
||||
const applyPreferences = () => {
|
||||
@@ -3169,18 +3171,15 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
|
||||
settings["i2p_outbound_length"] = Number(document.getElementById("i2pOutboundLength").value);
|
||||
|
||||
// Send it to qBT
|
||||
window.parent.qBittorrent.Cache.preferences.set({
|
||||
data: settings,
|
||||
onFailure: () => {
|
||||
alert("QBT_TR(Unable to save program preferences, qBittorrent is probably unreachable.)QBT_TR[CONTEXT=HttpServer]");
|
||||
window.parent.qBittorrent.Client.closeWindow(document.getElementById("preferencesPage"));
|
||||
},
|
||||
onSuccess: () => {
|
||||
window.parent.qBittorrent.Cache.preferences.set(settings)
|
||||
.then(() => {
|
||||
// Close window
|
||||
window.parent.location.reload();
|
||||
window.parent.qBittorrent.Client.closeWindow(document.getElementById("preferencesPage"));
|
||||
}
|
||||
});
|
||||
}).catch((error) => {
|
||||
alert("QBT_TR(Unable to save program preferences, qBittorrent is probably unreachable.)QBT_TR[CONTEXT=HttpServer]");
|
||||
window.parent.qBittorrent.Client.closeWindow(document.getElementById("preferencesPage"));
|
||||
});
|
||||
};
|
||||
|
||||
const setup = () => {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
*/
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QByteArrayView>
|
||||
#include <QLatin1StringView>
|
||||
#include <QObject>
|
||||
#include <QTest>
|
||||
@@ -123,6 +124,33 @@ private slots:
|
||||
QCOMPARE(Utils::ByteArray::toBase32("0000000000"), "GAYDAMBQGAYDAMBQ");
|
||||
QCOMPARE(Utils::ByteArray::toBase32("1"), "GE======");
|
||||
}
|
||||
|
||||
void testUnquote() const
|
||||
{
|
||||
const auto test = []<typename T>()
|
||||
{
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>({}), {});
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("abc"), "abc");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("\"abc\""), "abc");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("\"a b c\""), "a b c");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("\"abc"), "\"abc");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("abc\""), "abc\"");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>(" \"abc\" "), " \"abc\" ");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("\"a\"bc\""), "a\"bc");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("'abc'", "'"), "abc");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("'abc'", "\"'"), "abc");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("\"'abc'\"", "\"'"), "'abc'");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("\"'abc'\"", "'\""), "'abc'");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("\"'abc'\"", "'"), "\"'abc'\"");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("\"abc'", "'"), "\"abc'");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("'abc\"", "'"), "'abc\"");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("\"\""), "");
|
||||
QCOMPARE(Utils::ByteArray::unquote<T>("\""), "\"");
|
||||
};
|
||||
|
||||
test.template operator()<QByteArray>();
|
||||
test.template operator()<QByteArrayView>();
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(TestUtilsByteArray)
|
||||
|
||||
Reference in New Issue
Block a user