diff options
author | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2020-07-22 21:04:58 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-22 21:04:58 +0200 |
commit | 894755776f0d100032eb4346ea4a1f07defb61cb (patch) | |
tree | 89754430a385e9412732f1ba9ac99c8511edd831 /lib | |
parent | 9f9577ccdebad84faf96766f8e5b07e2f2b605c5 (diff) | |
parent | bd74588539d8a5922e9f51eb691052c06c02a5ed (diff) | |
download | libquotient-894755776f0d100032eb4346ea4a1f07defb61cb.tar.gz libquotient-894755776f0d100032eb4346ea4a1f07defb61cb.zip |
Merge pull request #407 from quotient-im/kitsune-resource-resolver
Matrix URIs and resolving them
Diffstat (limited to 'lib')
-rw-r--r-- | lib/quotient_common.h | 14 | ||||
-rw-r--r-- | lib/uri.cpp | 191 | ||||
-rw-r--r-- | lib/uri.h | 85 | ||||
-rw-r--r-- | lib/uriresolver.cpp | 112 | ||||
-rw-r--r-- | lib/uriresolver.h | 168 |
5 files changed, 563 insertions, 7 deletions
diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 44541b42..bb05af05 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, - UnknownMatrixId, - MalformedMatrixId, - NoAccount, - EmptyMatrixId + UriResolved = 0, + CouldNotResolve, + IncorrectAction, + InvalidUri, + NoAccount }; -Q_ENUM_NS(ResourceResolveResult) +Q_ENUM_NS(UriResolveResult) } // namespace Quotient /// \deprecated Use namespace Quotient instead diff --git a/lib/uri.cpp b/lib/uri.cpp new file mode 100644 index 00000000..f813794c --- /dev/null +++ b/lib/uri.cpp @@ -0,0 +1,191 @@ +#include "uri.h" + +#include "logging.h" + +#include <QtCore/QRegularExpression> + +using namespace Quotient; + +struct ReplacePair { QByteArray uriString; char sigil; }; +/// Defines bi-directional mapping of path prefixes and sigils +static const auto replacePairs = { + ReplacePair { "user/", '@' }, + { "roomid/", '!' }, + { "room/", '#' }, + // The notation for bare event ids is not proposed in MSC2312 but there's + // https://github.com/matrix-org/matrix-doc/pull/2644 + { "event/", '$' } +}; + +Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query) +{ + if (primaryId.isEmpty()) + primaryType_ = Empty; + else { + setScheme("matrix"); + QString pathToBe; + primaryType_ = Invalid; + if (primaryId.size() < 2) // There should be something after sigil + return; + for (const auto& p: replacePairs) + if (primaryId[0] == p.sigil) { + primaryType_ = Type(p.sigil); + pathToBe = p.uriString + primaryId.mid(1); + break; + } + if (!secondaryId.isEmpty()) { + if (secondaryId.size() < 2) { + primaryType_ = Invalid; + return; + } + pathToBe += "/event/" + secondaryId.mid(1); + } + setPath(pathToBe); + } + setQuery(std::move(query)); +} + +Uri::Uri(QUrl url) : QUrl(std::move(url)) +{ + // NB: don't try to use `url` from here on, it's moved-from and empty + if (isEmpty()) + return; // primaryType_ == Empty + + if (!QUrl::isValid()) { // MatrixUri::isValid() checks primaryType_ + primaryType_ = Invalid; + 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; // Invalid + } + + for (const auto& p: replacePairs) + if (urlPath.startsWith(p.uriString)) { + primaryType_ = Type(p.sigil); + return; // The only valid return path for matrix: URIs + } + qCDebug(MAIN) << "The matrix: URI is not recognised:" + << toDisplayString(); + return; + } + + primaryType_ = NonMatrix; // Default, unless overridden by the code below + 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 = Uri { m.captured("main").toUtf8(), + m.captured("sec").toUtf8(), m.captured("query") }; + } +} + +Uri::Uri(const QString& uriOrId) : Uri(fromUserInput(uriOrId)) {} + +Uri Uri::fromUserInput(const QString& uriOrId) +{ + if (uriOrId.isEmpty()) + return {}; // type() == None + + // A quick check if uriOrId is a plain Matrix id + // Bare event ids cannot be resolved without a room scope as per the current + // spec but there's a movement towards making them navigable (see, e.g., + // https://github.com/matrix-org/matrix-doc/pull/2644) - so treat them + // as valid + if (QStringLiteral("!@#+$").contains(uriOrId[0])) + return Uri { uriOrId.toUtf8() }; + + return Uri { QUrl::fromUserInput(uriOrId) }; +} + +Uri::Type Uri::type() const { return primaryType_; } + +Uri::SecondaryType Uri::secondaryType() const +{ + return path().section('/', 2, 2) == "event" ? EventId : NoSecondaryId; +} + +QUrl Uri::toUrl(UriForm form) const +{ + if (!isValid()) + return {}; + + if (form == CanonicalUri || type() == NonMatrix) + 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 Uri::primaryId() const +{ + if (primaryType_ == Empty || primaryType_ == Invalid) + return {}; + + const auto& idStem = path().section('/', 1, 1); + return idStem.isEmpty() ? idStem : primaryType_ + idStem; +} + +QString Uri::secondaryId() const +{ + const auto& idStem = path().section('/', 3); + return idStem.isEmpty() ? idStem : secondaryType() + idStem; +} + +static const auto ActionKey = QStringLiteral("action"); + +QString Uri::action() const +{ + return type() == NonMatrix || !isValid() + ? QString() + : QUrlQuery { query() }.queryItemValue(ActionKey); +} + +void Uri::setAction(const QString& newAction) +{ + if (!isValid()) { + qCWarning(MAIN) << "Cannot set an action on an invalid Quotient::Uri"; + return; + } + QUrlQuery q { query() }; + q.removeQueryItem(ActionKey); + q.addQueryItem(ActionKey, newAction); + setQuery(q); +} + +QStringList Uri::viaServers() const +{ + return QUrlQuery { query() }.allQueryItemValues(QStringLiteral("via"), + QUrl::EncodeReserved); +} + +bool Uri::isValid() const +{ + return primaryType_ != Empty && primaryType_ != Invalid; +} diff --git a/lib/uri.h b/lib/uri.h new file mode 100644 index 00000000..270766dd --- /dev/null +++ b/lib/uri.h @@ -0,0 +1,85 @@ +#pragma once + +#include "quotient_common.h" + +#include <QtCore/QUrl> +#include <QtCore/QUrlQuery> + +namespace Quotient { + +/*! \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 Uri : private QUrl { + Q_GADGET +public: + enum Type : char { + Invalid = char(-1), + Empty = 0x0, + UserId = '@', + RoomId = '!', + RoomAlias = '#', + Group = '+', + BareEventId = '$', // https://github.com/matrix-org/matrix-doc/pull/2644 + NonMatrix = ':' + }; + Q_ENUM(Type) + enum SecondaryType : char { NoSecondaryId = 0x0, EventId = '$' }; + Q_ENUM(SecondaryType) + + enum UriForm : short { CanonicalUri, MatrixToUri }; + Q_ENUM(UriForm) + + /// Construct an empty Matrix URI + Uri() = 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). + */ + Uri(const QString& uriOrId); + + /// Construct a Matrix URI from components + explicit Uri(QByteArray primaryId, QByteArray secondaryId = {}, + QString query = {}); + /// Construct a Matrix URI from matrix.to or MSC2312 (matrix:) URI + explicit Uri(QUrl url); + + static Uri fromUserInput(const QString& uriOrId); + static Uri 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. + */ + Q_INVOKABLE Type type() const; + Q_INVOKABLE SecondaryType secondaryType() const; + Q_INVOKABLE QUrl toUrl(UriForm form = CanonicalUri) const; + Q_INVOKABLE QString primaryId() const; + Q_INVOKABLE QString secondaryId() const; + Q_INVOKABLE QString action() const; + Q_INVOKABLE void setAction(const QString& newAction); + Q_INVOKABLE QStringList viaServers() const; + Q_INVOKABLE bool isValid() const; + using QUrl::path, QUrl::query, QUrl::fragment; + using QUrl::isEmpty, QUrl::toDisplayString; + +private: + Type primaryType_ = Empty; +}; + +} diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp new file mode 100644 index 00000000..ec30512c --- /dev/null +++ b/lib/uriresolver.cpp @@ -0,0 +1,112 @@ +#include "uriresolver.h" + +#include "connection.h" +#include "user.h" + +using namespace Quotient; + +UriResolveResult UriResolverBase::visitResource(Connection* account, + const Uri& uri) +{ + switch (uri.type()) { + case Uri::NonMatrix: + return visitNonMatrix(uri.toUrl()) ? UriResolved : CouldNotResolve; + case Uri::Invalid: + case Uri::Empty: + return InvalidUri; + default:; + } + + if (!account) + return NoAccount; + + switch (uri.type()) { + case Uri::UserId: { + if (uri.action() == "join") + return IncorrectAction; + auto* user = account->user(uri.primaryId()); + Q_ASSERT(user != nullptr); + return visitUser(user, uri.action()); + } + case Uri::RoomId: + case Uri::RoomAlias: { + auto* room = uri.type() == Uri::RoomId + ? account->room(uri.primaryId()) + : account->roomByAlias(uri.primaryId()); + if (room != nullptr) { + visitRoom(room, uri.secondaryId()); + return UriResolved; + } + if (uri.action() == "join") { + joinRoom(account, uri.primaryId(), uri.viaServers()); + return UriResolved; + } + [[fallthrough]]; + } + default: + return CouldNotResolve; + } +} + +// This template is only instantiated once, for Quotient::visitResource() +template <typename... FnTs> +class StaticUriDispatcher : public UriResolverBase { +public: + StaticUriDispatcher(const FnTs&... fns) : fns_(fns...) {} + +private: + UriResolveResult visitUser(User* user, const QString& action) override + { + return std::get<0>(fns_)(user, action); + } + void visitRoom(Room* room, const QString& eventId) override + { + std::get<1>(fns_)(room, eventId); + } + void joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers = {}) override + { + std::get<2>(fns_)(account, roomAliasOrId, viaServers); + } + bool visitNonMatrix(const QUrl& url) override + { + return std::get<3>(fns_)(url); + } + + std::tuple<FnTs...> fns_; +}; + +UriResolveResult Quotient::visitResource( + Connection* account, const Uri& uri, + std::function<UriResolveResult(User*, QString)> userHandler, + std::function<void(Room*, QString)> roomEventHandler, + std::function<void(Connection*, QString, QStringList)> joinHandler, + std::function<bool(const QUrl&)> nonMatrixHandler) +{ + return StaticUriDispatcher(userHandler, roomEventHandler, joinHandler, + nonMatrixHandler) + .visitResource(account, uri); +} + +UriResolveResult UriDispatcher::visitUser(User *user, const QString &action) +{ + emit userAction(user, action); + return UriResolved; +} + +void UriDispatcher::visitRoom(Room *room, const QString &eventId) +{ + emit roomAction(room, eventId); +} + +void UriDispatcher::joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers) +{ + emit joinAction(account, roomAliasOrId, viaServers); +} + +bool UriDispatcher::visitNonMatrix(const QUrl &url) +{ + emit nonMatrixAction(url); + return true; +} diff --git a/lib/uriresolver.h b/lib/uriresolver.h new file mode 100644 index 00000000..9b2ced9d --- /dev/null +++ b/lib/uriresolver.h @@ -0,0 +1,168 @@ +#pragma once + +#include "uri.h" + +#include <QtCore/QObject> + +#include <functional> + +namespace Quotient { +class Connection; +class Room; +class User; + +/*! \brief Abstract class to resolve the resource and act on it + * + * This class encapsulates the logic of resolving a Matrix identifier or URI + * into a Quotient object (or objects) and calling an appropriate handler on it. + * It is a type-safe way of handling a URI with no prior context on its type + * in cases like, e.g., when a user clicks on a URI in the application. + * + * This class provides empty "handlers" for each type of URI to facilitate + * gradual implementation. Derived classes are encouraged to override as many + * of them as possible. + */ +class UriResolverBase { +public: + /*! \brief Resolve the resource and dispatch an action depending on its type + * + * This method: + * 1. Resolves \p uri into an actual object (e.g., Room or User), + * with possible additional data such as event id, in the context of + * \p account. + * 2. If the resolving is successful, depending on the type of the object, + * calls the appropriate virtual function (defined in a derived + * concrete class) to perform an action on the resource (open a room, + * mention a user etc.). + * 3. Returns the result of resolving the resource. + */ + UriResolveResult visitResource(Connection* account, const Uri& uri); + +protected: + /// Called by visitResource() when the passed URI identifies a Matrix user + /*! + * \return IncorrectAction if the action is not correct or not supported; + * UriResolved if it is accepted; other values are disallowed + */ + virtual UriResolveResult visitUser(User* user, const QString& action) + { + return IncorrectAction; + } + /// Called by visitResource() when the passed URI identifies a room or + /// an event in a room + virtual void visitRoom(Room* room, const QString& eventId) {} + /// Called by visitResource() when the passed URI has `action() == "join"` + /// and identifies a room that the user defined by the Connection argument + /// is not a member of + virtual void joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers = {}) + {} + /// Called by visitResource() when the passed URI has `type() == NonMatrix` + /*! + * Should return true if the URI is considered resolved, false otherwise. + * A basic implementation in a graphical client can look like + * `return QDesktopServices::openUrl(url);` but it's strongly advised to + * ask for a user confirmation beforehand. + */ + virtual bool visitNonMatrix(const QUrl& url) { return false; } +}; + +/*! \brief Resolve the resource and invoke an action on it, via function objects + * + * This function encapsulates the logic of resolving a Matrix identifier or URI + * into a Quotient object (or objects) and calling an appropriate handler on it. + * Unlike UriResolverBase it accepts the list of handlers from + * the caller; internally it's uses a minimal UriResolverBase class + * + * \param account The connection used as a context to resolve the identifier + * + * \param uri A URI that can represent a Matrix entity + * + * \param userHandler Called when the passed URI identifies a Matrix user + * + * \param roomEventHandler Called when the passed URI identifies a room or + * an event in a room + * + * \param joinHandler Called when the passed URI has `action() == "join"` and + * identifies a room that the user defined by + * the Connection argument is not a member of + * + * \param nonMatrixHandler Called when the passed URI has `type() == NonMatrix`; + * should return true if the URI is considered resolved, + * false otherwise + * + * \sa UriResolverBase, UriDispatcher + */ +UriResolveResult +visitResource(Connection* account, const Uri& uri, + std::function<UriResolveResult(User*, QString)> userHandler, + std::function<void(Room*, QString)> roomEventHandler, + std::function<void(Connection*, QString, QStringList)> joinHandler, + std::function<bool(const QUrl&)> nonMatrixHandler); + +/*! \brief Check that the resource is resolvable with no action on it */ +inline UriResolveResult checkResource(Connection* account, const Uri& uri) +{ + return visitResource( + account, uri, [](auto, auto) { return UriResolved; }, [](auto, auto) {}, + [](auto, auto, auto) {}, [](auto) { return false; }); +} + +/*! \brief Resolve the resource and invoke an action on it, via Qt signals + * + * This is an implementation of UriResolverBase that is based on + * QObject and uses Qt signals instead of virtual functions to provide an + * open-ended interface for visitors. + * + * This class is aimed primarily at clients where invoking the resolving/action + * and handling the action are happening in decoupled parts of the code; it's + * also useful to operate on Matrix identifiers and URIs from QML/JS code + * that cannot call resolveResource() due to QML/C++ interface limitations. + * + * This class does not restrain the client code to a certain type of + * connections: both direct and queued (or a mix) will work fine. One limitation + * caused by that is there's no way to indicate if a non-Matrix URI has been + * successfully resolved - a signal always returns void. + * + * Note that in case of using (non-blocking) queued connections the code that + * calls resolveResource() should not expect the action to be performed + * synchronously - the returned value is the result of resolving the URI, + * not acting on it. + */ +class UriDispatcher : public QObject, public UriResolverBase { + Q_OBJECT +public: + explicit UriDispatcher(QObject* parent = nullptr) : QObject(parent) {} + + // It's actually UriResolverBase::visitResource() but with Q_INVOKABLE + Q_INVOKABLE UriResolveResult resolveResource(Connection* account, + const Uri& uri) + { + return UriResolverBase::visitResource(account, uri); + } + +signals: + /// An action on a user has been requested + void userAction(Quotient::User* user, QString action); + + /// An action on a room has been requested, with optional event id + void roomAction(Quotient::Room* room, QString eventId); + + /// A join action has been requested, with optional 'via' servers + void joinAction(Quotient::Connection* account, QString roomAliasOrId, + QStringList viaServers); + + /// An action on a non-Matrix URL has been requested + void nonMatrixAction(QUrl url); + +private: + UriResolveResult visitUser(User* user, const QString& action) override; + void visitRoom(Room* room, const QString& eventId) override; + void joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers = {}) override; + bool visitNonMatrix(const QUrl& url) override; +}; + +} // namespace Quotient + + |