aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/quotient_common.h8
-rw-r--r--lib/resourceresolver.cpp275
-rw-r--r--lib/resourceresolver.h172
-rw-r--r--tests/quotest.cpp65
4 files changed, 372 insertions, 148 deletions
diff --git a/lib/quotient_common.h b/lib/quotient_common.h
index 44541b42..446f628b 100644
--- a/lib/quotient_common.h
+++ b/lib/quotient_common.h
@@ -14,15 +14,15 @@ enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 };
Q_ENUM_NS(RunningPolicy)
-enum ResourceResolveResult : short {
+enum UriResolveResult : short {
StillResolving = -1,
- Resolved = 0,
+ UriResolved = 0,
UnknownMatrixId,
- MalformedMatrixId,
+ MalformedUri,
NoAccount,
EmptyMatrixId
};
-Q_ENUM_NS(ResourceResolveResult)
+Q_ENUM_NS(UriResolveResult)
} // namespace Quotient
/// \deprecated Use namespace Quotient instead
diff --git a/lib/resourceresolver.cpp b/lib/resourceresolver.cpp
index 0d5c5a20..e7820061 100644
--- a/lib/resourceresolver.cpp
+++ b/lib/resourceresolver.cpp
@@ -1,97 +1,236 @@
#include "resourceresolver.h"
-#include "settings.h"
+#include "connection.h"
+#include "logging.h"
#include <QtCore/QRegularExpression>
using namespace Quotient;
-QString ResourceResolver::toMatrixId(const QString& uriOrId,
- QStringList uriServers)
+struct ReplacePair { QByteArray uriString; char sigil; };
+static const auto replacePairs = { ReplacePair { "user/", '@' },
+ { "roomid/", '!' },
+ { "room/", '#' } };
+
+MatrixUri::MatrixUri(QByteArray primaryId, QByteArray secondaryId, QString query)
{
- auto id = QUrl::fromPercentEncoding(uriOrId.toUtf8());
- const auto MatrixScheme = "matrix:"_ls;
- if (id.startsWith(MatrixScheme)) {
- id.remove(0, MatrixScheme.size());
- for (const auto& p: { std::pair { "user/"_ls, '@' },
- { "roomid/"_ls, '!' },
- { "room/"_ls, '#' } })
- if (id.startsWith(p.first)) {
- id.replace(0, p.first.size(), p.second);
+ if (primaryId.isEmpty())
+ primaryType_ = Empty;
+ else {
+ setScheme("matrix");
+ QString pathToBe;
+ primaryType_ = Invalid;
+ for (const auto& p: replacePairs)
+ if (primaryId[0] == p.sigil) {
+ primaryType_ = Type(p.sigil);
+ pathToBe = p.uriString + primaryId.mid(1);
break;
}
- // The below assumes that /event/ cannot show up in normal Matrix ids.
- id.replace("/event/"_ls, "/$"_ls);
- } else {
- const auto MatrixTo_ServerName = QStringLiteral("matrix.to");
- if (!uriServers.contains(MatrixTo_ServerName))
- uriServers.push_back(MatrixTo_ServerName);
- id.remove(
- QRegularExpression("^https://(" + uriServers.join('|') + ")/?#/"));
+ if (!secondaryId.isEmpty())
+ pathToBe += "/event/" + secondaryId.mid(1);
+ setPath(pathToBe);
}
- return id;
+ setQuery(std::move(query));
}
-ResourceResolver::Result ResourceResolver::visitResource(
- Connection* account, const QString& identifier,
- std::function<void(User*)> userHandler,
- std::function<void(Room*, QString)> roomEventHandler)
+MatrixUri::MatrixUri(QUrl url) : QUrl(std::move(url))
{
- const auto& normalizedId = toMatrixId(identifier);
- auto&& [sigil, mainId, secondaryId] = parseIdentifier(normalizedId);
- Room* room = nullptr;
- switch (sigil) {
- case char(-1):
- return MalformedMatrixId;
- case char(0):
- return EmptyMatrixId;
- case '@':
- if (auto* user = account->user(mainId)) {
- userHandler(user);
- return Success;
- }
- return MalformedMatrixId;
- case '!':
- if ((room = account->room(mainId)))
- break;
- return UnknownMatrixId;
- case '#':
- if ((room = account->roomByAlias(mainId)))
+ // NB: url is moved from and empty by now
+ if (isEmpty())
+ return; // primaryType_ == None
+
+ primaryType_ = Invalid;
+ if (!QUrl::isValid()) // MatrixUri::isValid() checks primaryType_
+ return;
+
+ if (scheme() == "matrix") {
+ // Check sanity as per https://github.com/matrix-org/matrix-doc/pull/2312
+ const auto& urlPath = path();
+ const auto& splitPath = urlPath.splitRef('/');
+ switch (splitPath.size()) {
+ case 2:
+ break;
+ case 4:
+ if (splitPath[2] == "event")
break;
[[fallthrough]];
default:
- return UnknownMatrixId;
+ return; // Invalid
+ }
+
+ for (const auto& p: replacePairs)
+ if (urlPath.startsWith(p.uriString)) {
+ primaryType_ = Type(p.sigil);
+ return; // The only valid return path for matrix: URIs
+ }
+ qCWarning(MAIN) << "Invalid matrix: URI passed to MatrixUri";
+ }
+ if (scheme() == "https" && authority() == "matrix.to") {
+ // See https://matrix.org/docs/spec/appendices#matrix-to-navigation
+ static const QRegularExpression MatrixToUrlRE {
+ R"(^/(?<main>[^/?]+)(/(?<sec>[^?]+))?(\?(?<query>.+))?$)"
+ };
+ // matrix.to accepts both literal sigils (as well as & and ? used in
+ // its "query" substitute) and their %-encoded forms;
+ // so force QUrl to decode everything.
+ auto f = fragment(QUrl::FullyDecoded);
+ if (auto&& m = MatrixToUrlRE.match(f); m.hasMatch())
+ *this = MatrixUri { m.captured("main").toUtf8(),
+ m.captured("sec").toUtf8(),
+ m.captured("query") };
}
- roomEventHandler(room, secondaryId);
- return Success;
}
-ResourceResolver::IdentifierParts
-ResourceResolver::parseIdentifier(const QString& identifier)
+MatrixUri::MatrixUri(const QString &uriOrId)
+ : MatrixUri(fromUserInput(uriOrId))
+{ }
+
+MatrixUri MatrixUri::fromUserInput(const QString& uriOrId)
{
- if (identifier.isEmpty())
+ if (uriOrId.isEmpty())
+ return {}; // type() == None
+
+ // A quick check if uriOrId is a plain Matrix id
+ if (QStringLiteral("!@#+").contains(uriOrId[0]))
+ return MatrixUri { uriOrId.toUtf8() };
+
+ // Bare event ids cannot be resolved without a room scope but are treated as
+ // valid anyway; in the future we might expose them as, say,
+ // matrix:event/eventid
+ if (uriOrId[0] == '$')
+ return MatrixUri { "", uriOrId.toUtf8() };
+
+ return MatrixUri { QUrl::fromUserInput(uriOrId) };
+}
+
+MatrixUri::Type MatrixUri::type() const { return primaryType_; }
+
+MatrixUri::SecondaryType MatrixUri::secondaryType() const
+{
+ return path().section('/', 2, 2) == "event" ? EventId : NoSecondaryId;
+}
+
+QUrl MatrixUri::toUrl(UriForm form) const
+{
+ if (!isValid())
return {};
-
- // The regex is quick and dirty, only intending to triage the id.
- static const QRegularExpression IdRE {
- "^(?<main>(?<sigil>.)([^/]+))(/(?<sec>[^?]+))?"
- };
- auto dissectedId = IdRE.match(identifier);
- if (!dissectedId.hasMatch())
- return { char(-1) };
-
- const auto sigil = dissectedId.captured("sigil");
- return { sigil.size() != 1 ? char(-1) : sigil[0].toLatin1(),
- dissectedId.captured("main"), dissectedId.captured("sec") };
+
+ if (form == CanonicalUri)
+ return *this;
+
+ QUrl url;
+ url.setScheme("https");
+ url.setHost("matrix.to");
+ url.setPath("/");
+ auto fragment = primaryId();
+ if (const auto& secId = secondaryId(); !secId.isEmpty())
+ fragment += '/' + secId;
+ if (const auto& q = query(); !q.isEmpty())
+ fragment += '?' + q;
+ url.setFragment(fragment);
+ return url;
+}
+
+QString MatrixUri::toDisplayString(MatrixUri::UriForm form) const
+{
+ return toUrl(form).toDisplayString();
}
-ResourceResolver::Result
+QString MatrixUri::primaryId() const
+{
+ if (primaryType_ == Empty || primaryType_ == Invalid)
+ return {};
+
+ const auto& idStem = path().section('/', 1, 1);
+ return idStem.isEmpty() ? idStem : primaryType_ + idStem;
+}
+
+QString MatrixUri::secondaryId() const
+{
+ const auto& idStem = path().section('/', 3);
+ return idStem.isEmpty() ? idStem : secondaryType() + idStem;
+}
+
+QString MatrixUri::action() const
+{
+ return QUrlQuery { query() }.queryItemValue("action");
+}
+
+QStringList MatrixUri::viaServers() const
+{
+ return QUrlQuery { query() }.allQueryItemValues(QStringLiteral("via"),
+ QUrl::EncodeReserved);
+}
+
+bool MatrixUri::isValid() const
+{
+ return primaryType_ != Empty && primaryType_ != Invalid;
+}
+
+UriResolveResult Quotient::visitResource(
+ Connection* account, const MatrixUri& uri,
+ std::function<void(User*)> userHandler,
+ std::function<void(Room*, QString)> roomEventHandler,
+ std::function<void(Connection*, QString, QStringList)> joinHandler)
+{
+ Q_ASSERT_X(account != nullptr, __FUNCTION__,
+ "The Connection argument passed to visit/openResource must not "
+ "be nullptr");
+ if (uri.action() == "join") {
+ if (uri.type() != MatrixUri::RoomAlias
+ && uri.type() != MatrixUri::RoomId)
+ return MalformedUri;
+
+ joinHandler(account, uri.primaryId(), uri.viaServers());
+ return UriResolved;
+ }
+
+ Room* room = nullptr;
+ switch (uri.type()) {
+ case MatrixUri::Invalid:
+ return MalformedUri;
+ case MatrixUri::Empty:
+ return EmptyMatrixId;
+ case MatrixUri::UserId:
+ if (auto* user = account->user(uri.primaryId())) {
+ userHandler(user);
+ return UriResolved;
+ }
+ return MalformedUri;
+ case MatrixUri::RoomId:
+ if ((room = account->room(uri.primaryId())))
+ break;
+ return UnknownMatrixId;
+ case MatrixUri::RoomAlias:
+ if ((room = account->roomByAlias(uri.primaryId())))
+ break;
+ [[fallthrough]];
+ default:
+ return UnknownMatrixId;
+ }
+ roomEventHandler(room, uri.secondaryId());
+ return UriResolved;
+}
+
+UriResolveResult
ResourceResolver::openResource(Connection* account, const QString& identifier,
const QString& action)
{
- return visitResource(account, identifier,
- [this, &action](User* u) { emit userAction(u, action); },
- [this, &action](Room* room, const QString& eventId) {
- emit roomAction(room, eventId, action);
+ return openResource(account, MatrixUri(identifier), action);
+}
+
+UriResolveResult ResourceResolver::openResource(Connection* account,
+ const MatrixUri& uri,
+ const QString& overrideAction)
+{
+ return visitResource(
+ account, uri,
+ [this, &overrideAction](User* u) { emit userAction(u, overrideAction); },
+ [this, &overrideAction](Room* room, const QString& eventId) {
+ emit roomAction(room, eventId, overrideAction);
+ },
+ [this](Connection* account, const QString& roomAliasOrId,
+ const QStringList& viaServers) {
+ emit joinAction(account, roomAliasOrId, viaServers);
});
}
diff --git a/lib/resourceresolver.h b/lib/resourceresolver.h
index 794b7796..fea07e97 100644
--- a/lib/resourceresolver.h
+++ b/lib/resourceresolver.h
@@ -1,10 +1,118 @@
#pragma once
-#include "connection.h"
+#include "quotient_common.h"
+
+#include <QtCore/QUrl>
+#include <QtCore/QUrlQuery>
+#include <QtCore/QObject>
#include <functional>
namespace Quotient {
+class Connection;
+class Room;
+class User;
+
+/*! \brief A wrapper around a Matrix URI or identifier
+ *
+ * This class encapsulates a Matrix resource identifier, passed in either of
+ * 3 forms: a plain Matrix identifier (sigil, localpart, serverpart or, for
+ * modern event ids, sigil and base64 hash); an MSC2312 URI (aka matrix: URI);
+ * or a matrix.to URL. The input can be either encoded (serverparts with
+ * punycode, the rest with percent-encoding) or unencoded (in this case it is
+ * the caller's responsibility to resolve all possible ambiguities).
+ *
+ * The class provides functions to check the validity of the identifier,
+ * its type, and obtain components, also in either unencoded (for displaying)
+ * or encoded (for APIs) form.
+ */
+class MatrixUri : private QUrl {
+ Q_GADGET
+ Q_PROPERTY(QString primaryId READ primaryId CONSTANT)
+ Q_PROPERTY(QString secondaryId READ secondaryId CONSTANT)
+// Q_PROPERTY(QUrlQuery query READ query CONSTANT)
+ Q_PROPERTY(QString action READ action CONSTANT)
+// Q_PROPERTY(QStringList viaServers READ viaServers CONSTANT)
+public:
+ enum Type : char {
+ Invalid = char(-1),
+ Empty = 0x0,
+ UserId = '@',
+ RoomId = '!',
+ RoomAlias = '#',
+ Group = '+'
+ };
+ Q_ENUM(Type)
+ enum SecondaryType : char {
+ NoSecondaryId = 0x0,
+ EventId = '$'
+ };
+
+ enum UriForm : short { CanonicalUri, MatrixToUri };
+ Q_ENUM(UriForm)
+
+ /// Construct an empty Matrix URI
+ MatrixUri() = default;
+ /*! \brief Decode a user input string to a Matrix identifier
+ *
+ * Accepts plain Matrix ids, MSC2312 URIs (aka matrix: URIs) and
+ * matrix.to URLs. In case of URIs/URLs, it uses QUrl's TolerantMode
+ * parser to decode common mistakes/irregularities (see QUrl documentation
+ * for more details).
+ */
+ MatrixUri(const QString& uriOrId);
+
+ /// Construct a Matrix URI from components
+ explicit MatrixUri(QByteArray primaryId, QByteArray secondaryId = {},
+ QString query = {});
+ /// Construct a Matrix URI from matrix.to or MSC2312 (matrix:) URI
+ explicit MatrixUri(QUrl url);
+
+ static MatrixUri fromUserInput(const QString& uriOrId);
+ static MatrixUri fromUrl(QUrl url);
+
+ /// Get the primary type of the Matrix URI (user id, room id or alias)
+ /*! Note that this does not include an event as a separate type, since
+ * events can only be addressed inside of rooms, which, in turn, are
+ * addressed either by id or alias. If you need to check whether the URI
+ * is specifically an event URI, use secondaryType() instead.
+ */
+ Type type() const;
+ SecondaryType secondaryType() const;
+ QUrl toUrl(UriForm form = CanonicalUri) const;
+ QString toDisplayString(UriForm form = CanonicalUri) const;
+ QString primaryId() const;
+ QString secondaryId() const;
+ QString action() const;
+ QStringList viaServers() const;
+ bool isValid() const;
+ using QUrl::isEmpty, QUrl::path, QUrl::query, QUrl::fragment;
+
+private:
+
+ Type primaryType_ = Empty;
+};
+
+/*! \brief Resolve the resource and invoke an action on it, visitor style
+ *
+ * This template function encapsulates the logic of resolving a Matrix
+ * identifier or URI into a Quotient object (or objects) and applying an
+ * appropriate action handler from the set provided by the caller to it.
+ * A typical use case for that is opening a room or mentioning a user in
+ * response to clicking on a Matrix URI or identifier.
+ *
+ * \param account The connection used as a context to resolve the identifier
+ *
+ * \param uri The Matrix identifier or URI; MSC2312 URIs and classic Matrix IDs
+ * are supported
+ *
+ * \sa ResourceResolver
+ */
+UriResolveResult
+visitResource(Connection* account, const MatrixUri& uri,
+ std::function<void(User*)> userHandler,
+ std::function<void(Room*, QString)> roomEventHandler,
+ std::function<void(Connection*, QString, QStringList)> joinHandler);
/*! \brief Matrix resource resolver
* TODO: rewrite
@@ -22,51 +130,9 @@ namespace Quotient {
class ResourceResolver : public QObject {
Q_OBJECT
public:
- enum Result : short {
- StillResolving = -1,
- Success = 0,
- UnknownMatrixId,
- MalformedMatrixId,
- NoAccount,
- EmptyMatrixId
- };
- Q_ENUM(Result)
-
explicit ResourceResolver(QObject* parent = nullptr) : QObject(parent)
{ }
- /*! \brief Decode a URI to a Matrix identifier (or a room/event pair)
- *
- * This accepts plain Matrix ids, MSC2312 URIs (aka matrix: URIs) and
- * matrix.to URIs.
- *
- * \return a Matrix identifier as defined by the common identifier grammars
- * or a slash separated pair of Matrix identifiers if the original
- * uri/id pointed to an event in a room
- */
- static QString toMatrixId(const QString& uriOrId,
- QStringList uriServers = {});
-
- /*! \brief Resolve the resource and invoke an action on it, visitor style
- *
- * This template function encapsulates the logic of resolving a Matrix
- * identifier or URI into a Quotient object (or objects) and applying an
- * appropriate action handler from the set provided by the caller to it.
- * A typical use case for that is opening a room or mentioning a user in
- * response to clicking on a Matrix URI or identifier.
- *
- * \param account The connection used as a context to resolve the identifier
- *
- * \param identifier The Matrix identifier or URI. MSC2312 URIs and classic
- * Matrix ID scheme are supported.
- *
- * \sa ResourceResolver
- */
- static Result
- visitResource(Connection* account, const QString& identifier,
- std::function<void(User*)> userHandler,
- std::function<void(Room*, QString)> roomEventHandler);
-
/*! \brief Resolve the resource and request an action on it, signal style
*
* This method:
@@ -91,9 +157,12 @@ public:
* and also connect to ResourceFuture::ready() signal in order to process
* the result of resolving and action.
*/
- Q_INVOKABLE Result openResource(Connection* account,
- const QString& identifier,
- const QString& action = {});
+ Q_INVOKABLE UriResolveResult openResource(Connection* account,
+ const QString& identifier,
+ const QString& action = {});
+ Q_INVOKABLE UriResolveResult
+ openResource(Connection* account, const MatrixUri& uri,
+ const QString& overrideAction = {});
signals:
/// An action on a user has been requested
@@ -102,14 +171,9 @@ signals:
/// An action on a room has been requested, with optional event id
void roomAction(Quotient::Room* room, QString eventId, QString action);
-private:
- struct IdentifierParts {
- char sigil;
- QString mainId {};
- QString secondaryId = {};
- };
-
- static IdentifierParts parseIdentifier(const QString& identifier);
+ /// A join action has been requested, with optional 'via' servers
+ void joinAction(Quotient::Connection* account, QString roomAliasOrId,
+ QStringList viaServers);
};
} // namespace Quotient
diff --git a/tests/quotest.cpp b/tests/quotest.cpp
index 19aa5b85..f9e25284 100644
--- a/tests/quotest.cpp
+++ b/tests/quotest.cpp
@@ -627,13 +627,15 @@ TEST_IMPL(visitResources)
// This lambda returns true in case of error, false if it's fine so far
auto testResourceResolver = [this, thisTest](const QStringList& uris,
auto signal, auto* target,
- const QString& eventId = {}) {
+ QVariantList otherArgs = {}) {
int r = qRegisterMetaType<decltype(target)>();
Q_ASSERT(r != 0);
QSignalSpy spy(&rr, signal);
- for (const auto& uri: uris) {
- clog << "Resolving uri " << uri.toStdString() << endl;
- rr.openResource(connection(), uri, "action");
+ for (const auto& uriString: uris) {
+ MatrixUri uri { uriString };
+ clog << "Checking " << uriString.toStdString()
+ << " -> " << uri.toDisplayString().toStdString() << endl;
+ rr.openResource(connection(), uri);
if (spy.count() != 1) {
clog << "Wrong number of signal emissions (" << spy.count()
<< ')' << endl;
@@ -642,21 +644,19 @@ TEST_IMPL(visitResources)
const auto& emission = spy.front();
Q_ASSERT(emission.count() >= 2);
if (emission.front().value<decltype(target)>() != target) {
- clog << "Action on an incorrect target called" << endl;
+ clog << "Signal emitted with an incorrect target" << endl;
FAIL_TEST();
}
- if (emission.back() != "action") {
- clog << "Action wasn't passed" << endl;
- FAIL_TEST();
- }
- if (!eventId.isEmpty()) {
- const auto passedEvtId = (emission.cend() - 2)->toString();
- if (passedEvtId != eventId) {
- clog << "Event passed incorrectly (received "
- << passedEvtId.toStdString() << " instead of "
- << eventId.toStdString() << ')' << endl;
+ if (!otherArgs.empty()) {
+ if (emission.size() < otherArgs.size() + 1) {
+ clog << "Emission doesn't include all arguments" << endl;
FAIL_TEST();
}
+ for (auto i = 0; i < otherArgs.size(); ++i)
+ if (otherArgs[i] != emission[i + 1]) {
+ clog << "Mismatch in argument #" << i + 1 << endl;
+ FAIL_TEST();
+ }
}
spy.clear();
}
@@ -674,13 +674,10 @@ TEST_IMPL(visitResources)
Q_ASSERT(!eventId.isEmpty());
const QStringList roomUris {
- roomId,
- "matrix:roomid/" + roomId.mid(1),
- "https://matrix.to/#/" + roomId,
- roomAlias,
- "matrix:room/" + roomAlias.mid(1),
+ roomId, "matrix:roomid/" + roomId.mid(1),
+ "https://matrix.to/#/%21" + roomId.mid(1),
+ roomAlias, "matrix:room/" + roomAlias.mid(1),
"https://matrix.to/#/" + roomAlias,
- "https://matrix.to#/" + roomAlias, // Just in case
};
const QStringList userUris { userId, "matrix:user/" + userId.mid(1),
"https://matrix.to/#/" + userId };
@@ -688,13 +685,37 @@ TEST_IMPL(visitResources)
"matrix:room/" + roomAlias.mid(1) + "/event/" + eventId.mid(1),
"https://matrix.to/#/" + roomId + '/' + eventId
};
+ // The following URIs are not supposed to be actually joined (and even
+ // exist, as yet) - only to be syntactically correct
+ static const auto& joinRoomAlias =
+ QStringLiteral("unjoined:example.org"); // # will be added below
+ QString joinQuery { "?action=join" };
+ static const QStringList joinByAliasUris {
+ "matrix:room/" + joinRoomAlias + joinQuery,
+ "https://matrix.to/#/%23"/*`#`*/ + joinRoomAlias + joinQuery
+ };
+ static const auto& joinRoomId = QStringLiteral("!anyid:example.org");
+ static const QStringList viaServers { "matrix.org", "example.org" };
+ static const auto viaQuery =
+ std::accumulate(viaServers.cbegin(), viaServers.cend(), joinQuery,
+ [](QString q, const QString& s) {
+ return q + "&via=" + s;
+ });
+ static const QStringList joinByIdUris {
+ "matrix:roomid/" + joinRoomId.mid(1) + viaQuery,
+ "https://matrix.to/#/" + joinRoomId + viaQuery
+ };
// If any test breaks, the breaking call will return true, and further
// execution will be cut by ||'s short-circuiting
if (testResourceResolver(roomUris, &ResourceResolver::roomAction, room())
|| testResourceResolver(userUris, &ResourceResolver::userAction,
connection()->user())
|| testResourceResolver(eventUris, &ResourceResolver::roomAction,
- room(), eventId))
+ room(), { eventId })
+ || testResourceResolver(joinByAliasUris, &ResourceResolver::joinAction,
+ connection(), { '#' + joinRoomAlias })
+ || testResourceResolver(joinByIdUris, &ResourceResolver::joinAction,
+ connection(), { joinRoomId, viaServers }))
return true;
// TODO: negative cases
FINISH_TEST(true);