diff --git a/doc/xep.doc b/doc/xep.doc index c91952e11..8666277f5 100644 --- a/doc/xep.doc +++ b/doc/xep.doc @@ -47,6 +47,7 @@ Ongoing: - XEP-0009: Jabber-RPC (API is not finalized yet) - XEP-0060: Publish-Subscribe (Only basic IQ implemented) - XEP-0077: In-Band Registration (Only basic IQ implemented) +- XEP-0363: HTTP File Upload (v0.9.0) - XEP-0369: Mediated Information eXchange (MIX) (Only IQ queries implemented) (v0.14.2) - XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements (Only IQ queries implemented) (v0.4.0) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d8eda94d2..6af697863 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,6 +19,7 @@ set(INSTALL_HEADER_FILES base/QXmppDiscoveryIq.h base/QXmppElement.h base/QXmppEntityTimeIq.h + base/QXmppHttpUploadIq.h base/QXmppIbbIq.h base/QXmppIq.h base/QXmppJingleIq.h @@ -95,6 +96,7 @@ set(SOURCE_FILES base/QXmppDiscoveryIq.cpp base/QXmppElement.cpp base/QXmppEntityTimeIq.cpp + base/QXmppHttpUploadIq.cpp base/QXmppIbbIq.cpp base/QXmppIq.cpp base/QXmppJingleIq.cpp diff --git a/src/base/QXmppConstants.cpp b/src/base/QXmppConstants.cpp index d1be6a6fe..8c3295d5d 100644 --- a/src/base/QXmppConstants.cpp +++ b/src/base/QXmppConstants.cpp @@ -132,6 +132,8 @@ const char* ns_idle = "urn:xmpp:idle:1"; const char* ns_chat_markers = "urn:xmpp:chat-markers:0"; // XEP-0352: Client State Indication const char* ns_csi = "urn:xmpp:csi:0"; +// XEP-0363: HTTP File Upload +const char* ns_http_upload = "urn:xmpp:http:upload:0"; // XEP-0369: Mediated Information eXchange (MIX) const char* ns_mix = "urn:xmpp:mix:core:1"; const char* ns_mix_create_channel = "urn:xmpp:mix:core:1#create-channel"; diff --git a/src/base/QXmppConstants_p.h b/src/base/QXmppConstants_p.h index 04cb820f7..35d922473 100644 --- a/src/base/QXmppConstants_p.h +++ b/src/base/QXmppConstants_p.h @@ -144,6 +144,8 @@ extern const char* ns_idle; extern const char* ns_chat_markers; // XEP-0352: Client State Indication extern const char* ns_csi; +// XEP-0363: HTTP File Upload +extern const char* ns_http_upload; // XEP-0369: Mediated Information eXchange (MIX) extern const char* ns_mix; extern const char* ns_mix_create_channel; diff --git a/src/base/QXmppHttpUploadIq.cpp b/src/base/QXmppHttpUploadIq.cpp new file mode 100644 index 000000000..4754cd3e9 --- /dev/null +++ b/src/base/QXmppHttpUploadIq.cpp @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2008-2019 The QXmpp developers + * + * Authors: + * Linus Jahn + * + * Source: + * https://github.com/qxmpp-project/qxmpp + * + * This file is a part of QXmpp library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + */ + +#include +#include + +#include "QXmppHttpUploadIq.h" +#include "QXmppConstants_p.h" + +class QXmppHttpUploadRequestIqPrivate +{ +public: + QString fileName; + qint64 size; + QMimeType contentType; +}; + +QXmppHttpUploadRequestIq::QXmppHttpUploadRequestIq() + : d(new QXmppHttpUploadRequestIqPrivate()) +{ +} + +QXmppHttpUploadRequestIq::~QXmppHttpUploadRequestIq() +{ + delete d; +} + +/// Returns the file name of the file to be uploaded. + +QString QXmppHttpUploadRequestIq::fileName() const +{ + return d->fileName; +} + +/// Sets the file name. The upload service will use this to create the upload/ +/// download URLs. This may also differ from the actual file name to get a +/// different URL. It's not required to replace special characters (this is the +/// server's job). + +void QXmppHttpUploadRequestIq::setFileName(const QString &fileName) +{ + d->fileName = fileName; +} + +/// Returns the file's size in bytes. + +qint64 QXmppHttpUploadRequestIq::size() const +{ + return d->size; +} + +/// Sets the file's size in bytes. + +void QXmppHttpUploadRequestIq::setSize(qint64 size) +{ + d->size = size; +} + +/// Returns the (optional) MIME-type of the file. + +QMimeType QXmppHttpUploadRequestIq::contentType() const +{ + return d->contentType; +} + +/// Sets the MIME-type of the file. This is optional. + +void QXmppHttpUploadRequestIq::setContentType(const QMimeType &type) +{ + d->contentType = type; +} + +bool QXmppHttpUploadRequestIq::isHttpUploadRequestIq(const QDomElement &element) +{ + if (element.tagName() == "iq") { + QDomElement request = element.firstChildElement("request"); + return !request.isNull() && request.namespaceURI() == ns_http_upload; + } + return false; +} + +/// \cond +void QXmppHttpUploadRequestIq::parseElementFromChild(const QDomElement &element) +{ + QDomElement request = element.firstChildElement("request"); + d->fileName = request.attribute("filename"); + d->size = request.attribute("size").toLongLong(); + if (request.hasAttribute("content-type")) { + QMimeDatabase mimeDb; + QMimeType type = mimeDb.mimeTypeForName(request.attribute("content-type")); + if (!type.isDefault() && type.isValid()) + d->contentType = type; + } +} + +void QXmppHttpUploadRequestIq::toXmlElementFromChild(QXmlStreamWriter *writer) const +{ + writer->writeStartElement("request"); + writer->writeAttribute("xmlns", ns_http_upload); + // filename and size are required + writer->writeAttribute("filename", d->fileName); + writer->writeAttribute("size", QString::number(d->size)); + // content-type is optional + if (!d->contentType.isDefault() && d->contentType.isValid()) + writer->writeAttribute("content-type", d->contentType.name()); + writer->writeEndElement(); +} +/// \endcond + +class QXmppHttpUploadSlotIqPrivate +{ +public: + QUrl putUrl; + QUrl getUrl; + QMap putHeaders; +}; + +QXmppHttpUploadSlotIq::QXmppHttpUploadSlotIq() + : d(new QXmppHttpUploadSlotIqPrivate()) +{ +} + +QXmppHttpUploadSlotIq::~QXmppHttpUploadSlotIq() +{ + delete d; +} + +/// Returns the URL for uploading via. HTTP PUT. + +QUrl QXmppHttpUploadSlotIq::putUrl() const +{ + return d->putUrl; +} + +/// Sets the URL the client should use for uploading. + +void QXmppHttpUploadSlotIq::setPutUrl(const QUrl &putUrl) +{ + d->putUrl = putUrl; +} + +/// Returns the URL to where the file will be served. + +QUrl QXmppHttpUploadSlotIq::getUrl() const +{ + return d->getUrl; +} + +/// Sets the download URL. + +void QXmppHttpUploadSlotIq::setGetUrl(const QUrl &getUrl) +{ + d->getUrl = getUrl; +} + +/// Returns a map of header fields (header name -> value) that need to be +/// included in the PUT (upload) request. This won't contain any other fields +/// than: "Authorization", "Cookie" or "Expires". + +QMap QXmppHttpUploadSlotIq::putHeaders() const +{ + return d->putHeaders; +} + +/// Sets the header fields the client needs to include in the PUT (upload) +/// request. All fields other than "Authorization", "Cookie" or "Expires" will +/// be ignored. + +void QXmppHttpUploadSlotIq::setPutHeaders(const QMap &putHeaders) +{ + d->putHeaders.clear(); + for (QString &name : putHeaders.keys()) { + if (name == "Authorization" || name == "Cookie" || name == "Expires") + d->putHeaders[name] = putHeaders[name]; + } +} + +bool QXmppHttpUploadSlotIq::isHttpUploadSlotIq(const QDomElement &element) +{ + if (element.tagName() == "iq") { + QDomElement slot = element.firstChildElement("slot"); + return !slot.isNull() && slot.namespaceURI() == ns_http_upload; + } + return false; +} + +/// \cond +void QXmppHttpUploadSlotIq::parseElementFromChild(const QDomElement &element) +{ + QDomElement slot = element.firstChildElement("slot"); + QDomElement put = slot.firstChildElement("put"); + d->getUrl = QUrl::fromEncoded(slot.firstChildElement("get").attribute("url").toUtf8()); + d->putUrl = QUrl::fromEncoded(put.attribute("url").toUtf8()); + if (put.hasChildNodes()) { + QMap headers; + QDomElement header = put.firstChildElement("header"); + while (!header.isNull()) { + headers[header.attribute("name")] = header.text(); + + header = header.nextSiblingElement("header"); + } + + setPutHeaders(headers); + } +} + +void QXmppHttpUploadSlotIq::toXmlElementFromChild(QXmlStreamWriter *writer) const +{ + writer->writeStartElement("slot"); + writer->writeAttribute("xmlns", ns_http_upload); + + writer->writeStartElement("put"); + writer->writeAttribute("url", d->putUrl.toEncoded()); + if (!d->putHeaders.isEmpty()) { + for (const QString &name : d->putHeaders.keys()) { + writer->writeStartElement("header"); + writer->writeAttribute("name", name); + writer->writeCharacters(d->putHeaders[name]); + writer->writeEndElement(); + } + } + writer->writeEndElement(); + + writer->writeStartElement("get"); + writer->writeAttribute("url", d->getUrl.toEncoded()); + writer->writeEndElement(); + + writer->writeEndElement(); +} +/// \endcond diff --git a/src/base/QXmppHttpUploadIq.h b/src/base/QXmppHttpUploadIq.h new file mode 100644 index 000000000..47cd7595d --- /dev/null +++ b/src/base/QXmppHttpUploadIq.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2008-2019 The QXmpp developers + * + * Authors: + * Linus Jahn + * + * Source: + * https://github.com/qxmpp-project/qxmpp + * + * This file is a part of QXmpp library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + */ + +#ifndef QXMPPHTTPUPLOADIQ_H +#define QXMPPHTTPUPLOADIQ_H + +#include +#include +#include + +#include "QXmppIq.h" + +class QXmppHttpUploadRequestIqPrivate; +class QXmppHttpUploadSlotIqPrivate; + +/// \brief Represents an HTTP File Upload IQ for requesting an upload slot as +/// defined by XEP-0363: HTTP File Upload [v0.9.0]. +/// +/// \ingroup Stanzas + +class QXMPP_EXPORT QXmppHttpUploadRequestIq : public QXmppIq +{ +public: + QXmppHttpUploadRequestIq(); + ~QXmppHttpUploadRequestIq(); + + QString fileName() const; + void setFileName(const QString &filename); + + qint64 size() const; + void setSize(qint64 size); + + QMimeType contentType() const; + void setContentType(const QMimeType &type); + + static bool isHttpUploadRequestIq(const QDomElement &element); + +protected: + /// \cond + void parseElementFromChild(const QDomElement &element); + void toXmlElementFromChild(QXmlStreamWriter *writer) const; + /// \endcond + +private: + QXmppHttpUploadRequestIqPrivate* const d; +}; + +/// \brief Represents an HTTP File Upload IQ result for receiving an upload slot as +/// defined by XEP-0363: HTTP File Upload [v0.9.0]. +/// +/// \ingroup Stanzas + +class QXMPP_EXPORT QXmppHttpUploadSlotIq : public QXmppIq +{ +public: + QXmppHttpUploadSlotIq(); + ~QXmppHttpUploadSlotIq(); + + QUrl putUrl() const; + void setPutUrl(const QUrl &putUrl); + + QUrl getUrl() const; + void setGetUrl(const QUrl &getUrl); + + QMap putHeaders() const; + void setPutHeaders(const QMap &putHeaders); + + static bool isHttpUploadSlotIq(const QDomElement &element); + +protected: + /// \cond + void parseElementFromChild(const QDomElement &element); + void toXmlElementFromChild(QXmlStreamWriter *writer) const; + /// \endcond + +private: + QXmppHttpUploadSlotIqPrivate* const d; +}; + +#endif // QXMPPHTTPUPLOADIQ_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 794cd609f..9984af9c4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,6 +21,7 @@ add_simple_test(qxmppcarbonmanager) add_simple_test(qxmppdataform) add_simple_test(qxmppdiscoveryiq) add_simple_test(qxmppentitytimeiq) +add_simple_test(qxmpphttpuploadiq) add_simple_test(qxmppiceconnection) add_simple_test(qxmppiq) add_simple_test(qxmppjingleiq) diff --git a/tests/qxmpphttpuploadiq/tst_qxmpphttpuploadiq.cpp b/tests/qxmpphttpuploadiq/tst_qxmpphttpuploadiq.cpp new file mode 100644 index 000000000..d88a113cd --- /dev/null +++ b/tests/qxmpphttpuploadiq/tst_qxmpphttpuploadiq.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2008-2019 The QXmpp developers + * + * Author: + * Linus Jahn + * + * Source: + * https://github.com/qxmpp-project/qxmpp + * + * This file is a part of QXmpp library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + */ + +#include +#include "QXmppHttpUploadIq.h" +#include "util.h" + +class tst_QXmppHttpUploadIq : public QObject +{ + Q_OBJECT + +private slots: + void testRequest(); + void testIsRequest_data(); + void testIsRequest(); + void testSlot(); + void testIsSlot_data(); + void testIsSlot(); +}; + +void tst_QXmppHttpUploadIq::testRequest() +{ + const QByteArray xml( + "" + "" + "" + ); + + QXmppHttpUploadRequestIq iq; + parsePacket(iq, xml); + QCOMPARE(iq.fileName(), QString("très cool.jpg")); + QCOMPARE(iq.size(), 23456); + QCOMPARE(iq.contentType().name(), QString("image/jpeg")); + serializePacket(iq, xml); + + // test setters + iq.setFileName("icon.png"); + QCOMPARE(iq.fileName(), QString("icon.png")); + iq.setSize(23421337); + QCOMPARE(iq.size(), 23421337); + iq.setContentType(QMimeDatabase().mimeTypeForName("image/png")); + QCOMPARE(iq.contentType().name(), QString("image/png")); +} + +void tst_QXmppHttpUploadIq::testIsRequest_data() +{ + QTest::addColumn("xml"); + QTest::addColumn("isRequest"); + + QTest::newRow("wrong-stanza") + << QByteArray("") + << false; + QTest::newRow("empty-iq") + << QByteArray("") + << false; + QTest::newRow("wrong-ns") + << QByteArray("") + << false; + QTest::newRow("correct") + << QByteArray("") + << true; +} + +void tst_QXmppHttpUploadIq::testIsRequest() +{ + QFETCH(QByteArray, xml); + QFETCH(bool, isRequest); + + QDomDocument doc; + QCOMPARE(doc.setContent(xml, true), true); + QCOMPARE(QXmppHttpUploadRequestIq::isHttpUploadRequestIq(doc.documentElement()), isRequest); +} + +void tst_QXmppHttpUploadIq::testSlot() +{ + const QByteArray xml( + "" + "" + "" + "
Basic Base64String==
" + "
foo=bar; user=romeo
" + "
" + "" + "
" + "
" + ); + + QXmppHttpUploadSlotIq iq; + parsePacket(iq, xml); + QCOMPARE(iq.putUrl(), QUrl("https://upload.montague.tld/4a771ac1-f0b2-4a4a" + "-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg")); + QCOMPARE(iq.getUrl(), QUrl("https://download.montague.tld/4a771ac1-f0b2-4a" + "4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg")); + QMap headers; + headers["Authorization"] = "Basic Base64String=="; + headers["Cookie"] = "foo=bar; user=romeo"; + QCOMPARE(iq.putHeaders(), headers); + serializePacket(iq, xml); + + // test setters + iq.setGetUrl(QUrl("https://dl.example.org/user/file")); + QCOMPARE(iq.getUrl(), QUrl("https://dl.example.org/user/file")); + iq.setPutUrl(QUrl("https://ul.example.org/user/file")); + QCOMPARE(iq.putUrl(), QUrl("https://ul.example.org/user/file")); + QMap emptyMap; + iq.setPutHeaders(emptyMap); + QCOMPARE(iq.putHeaders(), emptyMap); + +} + +void tst_QXmppHttpUploadIq::testIsSlot_data() +{ + QTest::addColumn("xml"); + QTest::addColumn("isSlot"); + + QTest::newRow("wrong-stanza") + << QByteArray("") + << false; + QTest::newRow("empty-iq") + << QByteArray("") + << false; + QTest::newRow("wrong-ns") + << QByteArray("") + << false; + QTest::newRow("correct") + << QByteArray("") + << true; +} + +void tst_QXmppHttpUploadIq::testIsSlot() +{ + QFETCH(QByteArray, xml); + QFETCH(bool, isSlot); + + QDomDocument doc; + QCOMPARE(doc.setContent(xml, true), true); + QCOMPARE(QXmppHttpUploadSlotIq::isHttpUploadSlotIq(doc.documentElement()), isSlot); +} + +QTEST_MAIN(tst_QXmppHttpUploadIq) +#include "tst_qxmpphttpuploadiq.moc"