From d34fa901a822cba1a34cdc60fc42223accb53785 Mon Sep 17 00:00:00 2001 From: Linus Jahn Date: Mon, 22 Jul 2024 21:18:25 +0200 Subject: [PATCH] draft: Add QXmppUri for XMPP URI parsing --- src/CMakeLists.txt | 2 + src/base/QXmppUri.cpp | 355 ++++++++++++++++++++++++++++++++++++++++++ src/base/QXmppUri.h | 130 ++++++++++++++++ 3 files changed, 487 insertions(+) create mode 100644 src/base/QXmppUri.cpp create mode 100644 src/base/QXmppUri.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e3d32018e..4bf1ab01e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -90,6 +90,7 @@ set(INSTALL_HEADER_FILES base/QXmppTrustMessages.h base/QXmppUserTuneItem.h base/QXmppUtils.h + base/QXmppUri.h base/QXmppVCardIq.h base/QXmppVersionIq.h base/compat/QXmppSessionIq.h @@ -230,6 +231,7 @@ set(SOURCE_FILES base/QXmppTask.cpp base/QXmppThumbnail.cpp base/QXmppTrustMessages.cpp + base/QXmppUri.cpp base/QXmppUserTuneItem.cpp base/QXmppUtils.cpp base/QXmppVCardIq.cpp diff --git a/src/base/QXmppUri.cpp b/src/base/QXmppUri.cpp new file mode 100644 index 000000000..85a0e085b --- /dev/null +++ b/src/base/QXmppUri.cpp @@ -0,0 +1,355 @@ +// SPDX-FileCopyrightText: 2019 Linus Jahn +// SPDX-FileCopyrightText: 2019 Melvin Keskin +// SPDX-FileCopyrightText: 2020 Jonah BrĂ¼chert +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppUri.h" + +#include "QXmppUtils_p.h" + +#include "StringLiterals.h" + +#include + +#include + +using namespace QXmpp::Private; +using namespace QXmpp::Uri; + +constexpr QStringView SCHEME = u"xmpp"; +constexpr QStringView PREFIX = u"xmpp:"; +constexpr QChar QUERY_ITEM_DELIMITER = u';'; +constexpr QChar QUERY_ITEM_KEY_DELIMITER = u'='; + +// Query types representing actions, e.g. "join" in +// "xmpp:group@example.org?join" for joining a group chat +constexpr std::array QUERY_TYPES = { + QStringView(), + u"command", + u"disco", + u"invite", + u"join", + u"login", + u"message", + u"pubsub", + u"recvfile", + u"register", + u"remove", + u"roster", + u"sendfile", + u"subscribe", + u"trust-message", + u"unregister", + u"unsubscribe", + u"vcard" +}; + +// QXmppMessage types as strings +constexpr std::array MESSAGE_TYPES = { + u"error", + u"normal", + u"chat", + u"groupchat", + u"headline" +}; + +/// +/// Adds a key-value pair to a query if the value is not empty. +/// +/// \param query to which the key-value pair is added +/// \param key key of the value +/// \param value value of the key +/// +static void addKeyValuePairToQuery(QUrlQuery &query, const QString &key, QStringView value) +{ + if (!value.isEmpty()) { + query.addQueryItem(key, value.toString()); + } +} + +/// +/// Extracts the fully-encoded value of a query's key-value pair. +/// +/// \param query query containing the key-value pair +/// \param key of the searched value +/// +/// @return the value of the key +/// +static QString queryItemValue(const QUrlQuery &query, const QString &key) +{ + return query.queryItemValue(key, QUrl::FullyDecoded); +} + +namespace QXmpp::Uri { + +struct NoQuery { }; + +} // namespace QXmpp::Uri + +static void serializeUrlQuery(const NoQuery &, QUrlQuery &) { } + +static void serializeUrlQuery(const Message &message, QUrlQuery &query) +{ + query.addQueryItem(u"message"_s, {}); + + addKeyValuePairToQuery(query, u"from"_s, message.from); + addKeyValuePairToQuery(query, u"id"_s, message.id); + if (message.type) { + addKeyValuePairToQuery(query, u"type"_s, MESSAGE_TYPES.at(size_t(*message.type))); + } + addKeyValuePairToQuery(query, QStringLiteral("subject"), message.subject); + addKeyValuePairToQuery(query, QStringLiteral("body"), message.body); + addKeyValuePairToQuery(query, QStringLiteral("thread"), message.thread); +} + +static void serializeUrlQuery(const Login &login, QUrlQuery &query) +{ + query.addQueryItem(u"login"_s, {}); + addKeyValuePairToQuery(query, QStringLiteral("password"), login.password); +} + +static void serializeUrlQuery(const TrustMessage &trustMessage, QUrlQuery &query) +{ + query.addQueryItem(u"trust-message"_s, {}); + + addKeyValuePairToQuery(query, QStringLiteral("encryption"), trustMessage.encryption); + + for (auto &identifier : trustMessage.trustKeyIds) { + addKeyValuePairToQuery(query, QStringLiteral("trust"), identifier); + } + + for (auto &identifier : trustMessage.distrustKeyIds) { + addKeyValuePairToQuery(query, QStringLiteral("distrust"), identifier); + } +} + +Command parseCommandQuery(const QUrlQuery &q) +{ + return { + queryItemValue(q, u"node"_s), + queryItemValue(q, u"action"_s), + }; +} + +Invite parseInviteQuery(const QUrlQuery &q) +{ + return { + queryItemValue(q, u"invite"_s), + queryItemValue(q, u"password"_s), + }; +} + +Join parseJoinQuery(const QUrlQuery &q) +{ + return { + queryItemValue(q, u"password"_s), + }; +} + +Login parseLoginQuery(const QUrlQuery &q) +{ + return { + queryItemValue(q, u"login"_s), + }; +} + +Message parseMessageQuery(const QUrlQuery &q) +{ + Message message; + message.subject = queryItemValue(q, u"subject"_s); + message.body = queryItemValue(q, u"body"_s); + message.thread = queryItemValue(q, u"thread"_s); + message.id = queryItemValue(q, u"id"_s); + message.from = queryItemValue(q, u"from"_s); + message.type = enumFromString(MESSAGE_TYPES, queryItemValue(q, u"type"_s)); +} + +TrustMessage parseTrustMessageQuery(const QUrlQuery &q) +{ + return TrustMessage { + queryItemValue(q, QStringLiteral("encryption")), + q.allQueryItemValues(QStringLiteral("trust"), QUrl::FullyDecoded), + q.allQueryItemValues(QStringLiteral("distrust"), QUrl::FullyDecoded), + }; +} + +struct QXmppUriPrivate : QSharedData { + QString jid; + std::any query; +}; + +/// +/// Parses the URI from a string. +/// +/// \param input string which may present an XMPP URI +/// +QXmppUri::QXmppUri() + : d(new QXmppUriPrivate) +{ +} + +QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppUri) + +std::optional QXmppUri::fromString(const QString &input) +{ + QUrl url(input); + if (!url.isValid() || url.scheme() != SCHEME) { + return {}; + } + + QXmppUri uri; + + QString jid = url.path(); + + if (!url.hasQuery()) { + return {}; + } + QUrlQuery urlQuery; + urlQuery.setQueryDelimiters(QUERY_ITEM_KEY_DELIMITER, QUERY_ITEM_DELIMITER); + urlQuery.setQuery(url.query(QUrl::FullyEncoded)); + + // Check if there is at least one query item. + if (!urlQuery.isEmpty()) { + auto queryItems = urlQuery.queryItems(); + Q_ASSERT(!queryItems.isEmpty()); + auto [queryString, queryValue] = queryItems.first(); + + if (!queryValue.isEmpty()) { + // invalid XMPP URI: first query query pair must have only key, no value + return {}; + } + + if (queryString == u"command") { + } else if (queryString == u"invite") { + uri.d->query = parseInviteQuery(urlQuery); + } else if (queryString == u"join") { + uri.d->query = parseJoinQuery(urlQuery); + } else if (queryString == u"login") { + uri.d->query = parseLoginQuery(urlQuery); + } else if (queryString == u"message") { + uri.d->query = parseMessageQuery(urlQuery); + } else if (queryString == u"register") { + uri.d->query = Register(); + } else if (queryString == u"remove") { + uri.d->query = Remove(); + } else if (queryString == u"roster") { + uri.d->query = parseRosterQuery(urlQuery); + } else if (queryString == u"subscribe") { + uri.d->query = Subscribe(); + } else if (queryString == u"trust-message") { + uri.d->query = parseTrustMessageQuery(urlQuery); + } else if (queryString == u"unregister") { + uri.d->query = Unregister(); + } else if (queryString == u"unsubscribe") { + uri.d->query = Unsubscribe(); + } else if (queryString == u"vcard") { + } else { + uri.d->query = parseCustomQuery(urlQuery); + } + /* + None, + Command, + Disco, + Invite, + Join, + Login, + Message, + PubSub, + RecvFile, + Register, + Remove, + Roster, + SendFile, + Subscribe, + TrustMessage, + Unregister, + Unsubscribe, + VCard, + */ + } + + return uri; +} + +template +bool serialize(const std::any &query, QUrlQuery &urlQuery) +{ + if (query.type() == typeid(T)) { + serializeUrlQuery(std::any_cast(query), urlQuery); + return true; + } + return false; +} + +/// +/// Serializes this URI to a string. +/// +/// \return this URI as a string +/// +QString QXmppUri::toString() +{ + QUrl url; + url.setScheme(SCHEME.toString()); + url.setPath(d->jid); + + // add URI query + QUrlQuery urlQuery; + urlQuery.setQueryDelimiters(QUERY_ITEM_KEY_DELIMITER, QUERY_ITEM_DELIMITER); + + if (d->query.has_value()) { + serialize(d->query, urlQuery) || + serialize(d->query, urlQuery) || + serialize(d->query, urlQuery) || + serialize(d->query, urlQuery) || + serialize(d->query, urlQuery) || + serialize(d->query, urlQuery) || + serialize(d->query, urlQuery) || + serialize(d->query, urlQuery) || + serialize(d->query, urlQuery) || + serialize(d->query, urlQuery) || + serialize(d->query, urlQuery) || + serialize(d->query, urlQuery) || + serialize(d->query, urlQuery); + } + + url.setQuery(urlQuery); + + return QString::fromUtf8(url.toEncoded(QUrl::FullyEncoded)); +} + +/// +/// Returns the JID this URI is about. +/// +/// This can also be e.g. a MUC room in case of a Join action. +/// +QString QXmppUri::jid() const +{ + return d->jid; +} + +/// +/// Sets the JID this URI links to. +/// +/// \param jid JID to be set +/// +void QXmppUri::setJid(const QString &jid) +{ + d->jid = jid; +} + +/// +/// Returns the query of the URI. +/// +/// It may be empty (has_value() returns false). Possible URI types are available in the namespace +/// QXmpp::Uri. +/// +std::any QXmppUri::query() const +{ + return d->query; +} + +void QXmppUri::setQuery(std::any &&query) +{ + d->query = std::move(query); +} diff --git a/src/base/QXmppUri.h b/src/base/QXmppUri.h new file mode 100644 index 000000000..1fa88a1e6 --- /dev/null +++ b/src/base/QXmppUri.h @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2019 Linus Jahn +// SPDX-FileCopyrightText: 2019 Melvin Keskin +// SPDX-FileCopyrightText: 2020 Jonah BrĂ¼chert +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPURI_H +#define QXMPPURI_H + +#include + +class QUrlQuery; + +struct QXmppUriPrivate; + +namespace QXmpp::Uri { + +struct Command { + QString node; + QString action; +}; + +struct Invite { + QString inviteeJid; + QString password; +}; + +struct Join { + QString password; +}; + +struct Login { + QString password; +}; + +struct Message { + QString to; + QString subject; + QString body; + QString thread; + QString id; + QString from; + std::optional type; +}; + +struct Unregister { }; + +struct Unsubscribe { }; + +struct Register { }; + +struct Remove { }; + +struct Roster { + QString name; + QString group; +}; + +struct Subscribe { }; + +struct TrustMessage { + QString encryption; + QList trustKeyIds; + QList distrustKeyIds; +}; + +struct CustomQuery { + QString query; + QVector> parameters; +}; + +} // namespace QXmpp::Uri + +/// +/// This class represents an XMPP URI as specified by RFC 5122 - +/// Internationalized Resource Identifiers (IRIs) and Uniform Resource +/// Identifiers (URIs) for the Extensible Messaging and Presence Protocol +/// (XMPP) and XEP-0147: XMPP URI Scheme Query Components. +/// +/// A QUrlQuery is used by this class to represent a query (component) of an +/// XMPP URI. A query conisists of query items which can be the query type or a +/// key-value pair. +/// +/// A query type is used to perform an action while the key-value pairs are +/// used to define its behavior. +/// +/// Example: +/// xmpp:alice@example.org?message;subject=Hello +/// +/// query (component): message;subject=Hello;body=world +/// query items: message, subject=Hello, body=world +/// query type: message +/// key-value pair 1: subject=Hello +/// key-value pair 2: body=world +/// +class QXmppUri +{ +public: + QXmppUri(); + QXMPP_PRIVATE_DECLARE_RULE_OF_SIX(QXmppUri) + + static std::optional fromString(const QString &); + + QString toString(); + + QString jid() const; + void setJid(const QString &jid); + + std::any query() const; + void setQuery(QXmpp::Uri::Command &&q) { setQuery(std::any(std::move(q))); } + void setQuery(QXmpp::Uri::Invite &&q) { setQuery(std::any(std::move(q))); } + void setQuery(QXmpp::Uri::Join &&q) { setQuery(std::any(std::move(q))); } + void setQuery(QXmpp::Uri::Login &&q) { setQuery(std::any(std::move(q))); } + void setQuery(QXmpp::Uri::Message &&q) { setQuery(std::any(std::move(q))); } + void setQuery(QXmpp::Uri::Unregister &&q) { setQuery(std::any(std::move(q))); } + void setQuery(QXmpp::Uri::Register &&q) { setQuery(std::any(std::move(q))); } + void setQuery(QXmpp::Uri::Remove &&q) { setQuery(std::any(std::move(q))); } + void setQuery(QXmpp::Uri::Roster &&q) { setQuery(std::any(std::move(q))); } + void setQuery(QXmpp::Uri::Subscribe &&q) { setQuery(std::any(std::move(q))); } + void setQuery(QXmpp::Uri::TrustMessage &&q) { setQuery(std::any(std::move(q))); } + void setQuery(QXmpp::Uri::CustomQuery &&q) { setQuery(std::any(std::move(q))); } + void resetQuery() { setQuery(std::any()); } + +private: + void setQuery(std::any &&); + + QSharedDataPointer d; +}; + +#endif // QXMPPURI_H