From 2da1df5518b26d423b1c92829195059698d18235 Mon Sep 17 00:00:00 2001 From: Linus Jahn Date: Wed, 20 Mar 2024 21:45:12 +0100 Subject: [PATCH] Update XEP-0428: Fallback Indication to v0.2 Introduces new QXmppFallback class for providing the new body/subject text references. https://xmpp.org/extensions/xep-0428.html --- doc/doap.xml | 2 +- src/CMakeLists.txt | 1 + src/base/QXmppFallback.h | 57 ++++++++ src/base/QXmppMessage.cpp | 174 ++++++++++++++++++++++-- src/base/QXmppMessage.h | 9 +- tests/qxmppmessage/tst_qxmppmessage.cpp | 45 +++++- 6 files changed, 270 insertions(+), 18 deletions(-) create mode 100644 src/base/QXmppFallback.h diff --git a/doc/doap.xml b/doc/doap.xml index 027b3dc28..13763dcd3 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -608,7 +608,7 @@ SPDX-License-Identifier: CC0-1.0 complete - 0.1 + 0.2.0 1.3 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 143f1dee8..2a2e273db 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -30,6 +30,7 @@ set(INSTALL_HEADER_FILES base/QXmppExtension.h base/QXmppExternalService.h base/QXmppExternalServiceDiscoveryIq.h + base/QXmppFallback.h base/QXmppFileMetadata.h base/QXmppFileShare.h base/QXmppFutureUtils_p.h diff --git a/src/base/QXmppFallback.h b/src/base/QXmppFallback.h new file mode 100644 index 000000000..dfa63378c --- /dev/null +++ b/src/base/QXmppFallback.h @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2024 Linus Jahn +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPFALLBACK_H +#define QXMPPFALLBACK_H + +#include "QXmppGlobal.h" + +#include + +#include + +class QDomElement; +class QXmlStreamWriter; + +struct QXmppFallbackPrivate; + +class QXMPP_EXPORT QXmppFallback +{ +public: + enum Element { + Body, + Subject, + }; + + struct Range { + /// Start index of the range + uint32_t start; + /// End index of the range + uint32_t end; + }; + + struct Reference { + /// Element of the message stanza this refers to + Element element; + /// Optional character range in the text + std::optional range; + }; + + QXmppFallback(const QString &forNamespace, const QVector &references); + QXMPP_PRIVATE_DECLARE_RULE_OF_SIX(QXmppFallback) + + const QString &forNamespace() const; + void setForNamespace(const QString &); + + const QVector &references() const; + void setReferences(const QVector &); + + static std::optional fromDom(const QDomElement &); + void toXml(QXmlStreamWriter *) const; + +private: + QSharedDataPointer d; +}; + +#endif // QXMPPFALLBACK_H diff --git a/src/base/QXmppMessage.cpp b/src/base/QXmppMessage.cpp index 5e82d59f5..199c70d28 100644 --- a/src/base/QXmppMessage.cpp +++ b/src/base/QXmppMessage.cpp @@ -10,6 +10,7 @@ #include "QXmppBitsOfBinaryDataList.h" #include "QXmppConstants_p.h" +#include "QXmppFallback.h" #include "QXmppFileShare.h" #include "QXmppGlobal_p.h" #include "QXmppJingleData.h" @@ -158,7 +159,7 @@ class QXmppMessagePrivate : public QSharedData std::optional mixInvitation; // XEP-0428: Fallback Indication - bool isFallback; + QVector fallbackMarkers; // XEP-0434: Trust Messages (TM) std::optional trustMessageElement; @@ -184,8 +185,7 @@ QXmppMessagePrivate::QXmppMessagePrivate() markable(false), marker(QXmppMessage::NoMarker), hints(0), - isSpoiler(false), - isFallback(false) + isSpoiler(false) { } @@ -1211,31 +1211,53 @@ void QXmppMessage::setMixInvitation(const std::optional &mix } /// -/// Sets whether this message is only a fallback according to \xep{0428}: -/// Fallback Indication. +/// Sets whether this message is only a fallback according to \xep{0428, Fallback Indication}. /// /// This is useful for clients not supporting end-to-end encryption to indicate /// that the message body does not contain the intended text of the author. /// +/// \deprecated Use fallbackMarkers() instead, it provides full access to the fallback elements. +/// /// \since QXmpp 1.3 /// bool QXmppMessage::isFallback() const { - return d->isFallback; + return !d->fallbackMarkers.empty(); } /// -/// Sets whether this message is only a fallback according to \xep{0428}: -/// Fallback Indication. +/// Sets whether this message is only a fallback according to \xep{0428, Fallback Indication}. /// /// This is useful for clients not supporting end-to-end encryption to indicate /// that the message body does not contain the intended text of the author. /// +/// \deprecated Use setFallbackMarkers() instead, it provides full access to the fallback elements. +/// /// \since QXmpp 1.3 /// void QXmppMessage::setIsFallback(bool isFallback) { - d->isFallback = isFallback; + d->fallbackMarkers = { QXmppFallback { {}, {} } }; +} + +/// +/// Returns the fallback elements defined in \xep{0428, Fallback Indication}. +/// +/// \since QXmpp 1.7 +/// +const QVector &QXmppMessage::fallbackMarkers() const +{ + return d->fallbackMarkers; +} + +/// +/// Sets the fallback elements defined in \xep{0428, Fallback Indication}. +/// +/// \since QXmpp 1.7 +/// +void QXmppMessage::setFallbackMarkers(const QVector &fallbackMarkers) +{ + d->fallbackMarkers = fallbackMarkers; } /// @@ -1464,7 +1486,9 @@ bool QXmppMessage::parseExtension(const QDomElement &element, QXmpp::SceMode sce #endif // XEP-0428: Fallback Indication if (checkElement(element, u"fallback", ns_fallback_indication)) { - d->isFallback = true; + if (auto fallback = QXmppFallback::fromDom(element)) { + d->fallbackMarkers.push_back(std::move(*fallback)); + } return true; } // XEP-0482: Call Invites @@ -1716,10 +1740,8 @@ void QXmppMessage::serializeExtensions(QXmlStreamWriter *writer, QXmpp::SceMode #endif // XEP-0428: Fallback Indication - if (d->isFallback) { - writer->writeStartElement(QSL65("fallback")); - writer->writeDefaultNamespace(toString65(ns_fallback_indication)); - writer->writeEndElement(); + for (const auto &fallback : d->fallbackMarkers) { + fallback.toXml(writer); } } @@ -1903,3 +1925,127 @@ void QXmppMessage::serializeExtensions(QXmlStreamWriter *writer, QXmpp::SceMode } } } + +struct QXmppFallbackPrivate : QSharedData { + QString forNamespace; + QVector references; +}; + +/// +/// \class QXmppFallback +/// +/// Fallback marker for message stanzas as defined in \xep{0428, Fallback Indication}. +/// +/// \sa QXmppFallback +/// \since QXmpp 1.7 +/// + +/// +/// \enum QXmppFallback::Element +/// +/// Describes the element of the message stanza this refers to. +/// + +/// +/// \struct QXmppFallback::Range +/// +/// A character range of a string, see \xep{0426, Character counting in message bodies} for details. +/// + +/// +/// \struct QXmppFallback::Reference +/// +/// A reference to a text in the message stanza. +/// + +QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppFallback) + +/// Creates a fallback marker. +QXmppFallback::QXmppFallback(const QString &forNamespace, const QVector &references) + : d(new QXmppFallbackPrivate { {}, forNamespace, references }) +{ +} + +/// +/// Returns the namespace of the XEP that this fallback marker is referring to. +/// +const QString &QXmppFallback::forNamespace() const +{ + return d->forNamespace; +} + +/// +/// Sets the namespace of the XEP that this fallback marker is referring to. +/// +void QXmppFallback::setForNamespace(const QString &ns) +{ + d->forNamespace = ns; +} + +/// +/// Returns the text references of this fallback marker. +/// +const QVector &QXmppFallback::references() const +{ + return d->references; +} + +/// +/// Sets the text references of this fallback marker. +/// +void QXmppFallback::setReferences(const QVector &references) +{ + d->references = references; +} + +/// +/// Tries to parse \a el into a QXmppFallback object. +/// +/// \return Empty optional on failure, parsed object otherwise. +/// +std::optional QXmppFallback::fromDom(const QDomElement &el) +{ + if (el.tagName() != u"fallback" || el.namespaceURI() != ns_fallback_indication) { + return {}; + } + + QVector references; + for (const auto &subEl : iterChildElements(el, {}, ns_fallback_indication)) { + auto start = parseInt(subEl.attribute(QStringLiteral("start"))); + auto end = parseInt(subEl.attribute(QStringLiteral("end"))); + std::optional range; + if (start && end) { + range = Range { *start, *end }; + } + + if (subEl.tagName() == u"body") { + references.push_back(Reference { Body, range }); + } else if (subEl.tagName() == u"subject") { + references.push_back(Reference { Subject, range }); + } + } + + return QXmppFallback { + el.attribute(QStringLiteral("for")), + references, + }; +} + +/// +/// Serializes the object to XML. +/// +void QXmppFallback::toXml(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QSL65("fallback")); + writer->writeDefaultNamespace(toString65(ns_fallback_indication)); + writeOptionalXmlAttribute(writer, u"for", d->forNamespace); + for (const auto &reference : d->references) { + writer->writeStartElement(reference.element == Body ? QSL65("body") : QSL65("subject")); + if (reference.range) { + writer->writeAttribute(QSL65("start"), serializeInt(reference.range->start)); + writer->writeAttribute(QSL65("end"), serializeInt(reference.range->end)); + } + writer->writeEndElement(); + } + writer->writeEndElement(); +} diff --git a/src/base/QXmppMessage.h b/src/base/QXmppMessage.h index 5d9d8dd95..416a8bed1 100644 --- a/src/base/QXmppMessage.h +++ b/src/base/QXmppMessage.h @@ -19,6 +19,7 @@ class QXmppMessagePrivate; class QXmppBitsOfBinaryDataList; +class QXmppFallback; class QXmppJingleMessageInitiationElement; class QXmppMessageReaction; class QXmppMixInvitation; @@ -254,8 +255,12 @@ class QXMPP_EXPORT QXmppMessage : public QXmppStanza void setMixInvitation(const std::optional &mixInvitation); // XEP-0428: Fallback Indication - bool isFallback() const; - void setIsFallback(bool isFallback); +#if QXMPP_DEPRECATED_SINCE(1, 7) + [[deprecated("Use fallbackMarkers()")]] bool isFallback() const; + [[deprecated("Use setFallbackMarkers()")]] void setIsFallback(bool isFallback); +#endif + const QVector &fallbackMarkers() const; + void setFallbackMarkers(const QVector &); // XEP-0434: Trust Messages (TM) std::optional trustMessageElement() const; diff --git a/tests/qxmppmessage/tst_qxmppmessage.cpp b/tests/qxmppmessage/tst_qxmppmessage.cpp index d923ca172..239065a05 100644 --- a/tests/qxmppmessage/tst_qxmppmessage.cpp +++ b/tests/qxmppmessage/tst_qxmppmessage.cpp @@ -8,6 +8,7 @@ #include "QXmppBitsOfBinaryContentId.h" #include "QXmppBitsOfBinaryDataList.h" #include "QXmppEncryptedFileSource.h" +#include "QXmppFallback.h" #include "QXmppJingleData.h" #include "QXmppMessage.h" #include "QXmppMessageReaction.h" @@ -1003,11 +1004,15 @@ void tst_QXmppMessage::testBobData() void tst_QXmppMessage::testFallbackIndication() { - const QByteArray xml = QByteArrayLiteral( + using Fallback = QXmppFallback; + + QByteArray xml = QByteArrayLiteral( "" "" ""); + QT_WARNING_PUSH + QT_WARNING_DISABLE_DEPRECATED QXmppMessage message; parsePacket(message, xml); QVERIFY(message.isFallback()); @@ -1016,6 +1021,44 @@ void tst_QXmppMessage::testFallbackIndication() QXmppMessage message2; message2.setIsFallback(true); serializePacket(message2, xml); + QT_WARNING_POP + + xml = + "" + "" + "" + "" + "" + "" + ""; + message = {}; + parsePacket(message, xml); + QCOMPARE(message.fallbackMarkers().size(), 1); + auto marker = message.fallbackMarkers().first(); + QCOMPARE(marker.forNamespace(), "urn:xmpp:reply:0"); + const auto &refs = marker.references(); + QCOMPARE(refs.size(), 3); + QCOMPARE(refs.at(0).element, Fallback::Body); + QCOMPARE(refs.at(1).element, Fallback::Subject); + QCOMPARE(refs.at(2).element, Fallback::Body); + QVERIFY(refs.at(0).range.has_value()); + QVERIFY(refs.at(1).range.has_value()); + QVERIFY(!refs.at(2).range.has_value()); + QCOMPARE(refs.at(0).range->start, 0); + QCOMPARE(refs.at(0).range->end, 33); + + serializePacket(message, xml); + + message = {}; + message.setFallbackMarkers({ + Fallback { + "urn:xmpp:reply:0", + { Fallback::Reference { Fallback::Body, Fallback::Range { 0, 33 } }, + Fallback::Reference { Fallback::Subject, Fallback::Range { 5, 10 } }, + Fallback::Reference { Fallback::Body, {} } }, + }, + }); + serializePacket(message, xml); } void tst_QXmppMessage::testStanzaIds()