Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Matrix URIs and resolving them #407

Merged
merged 10 commits into from
Jul 22, 2020
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ set(lib_SRCS
lib/room.cpp
lib/user.cpp
lib/avatar.cpp
lib/uri.cpp
lib/uriresolver.cpp
lib/syncdata.cpp
lib/settings.cpp
lib/networksettings.cpp
Expand Down
14 changes: 7 additions & 7 deletions lib/quotient_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
191 changes: 191 additions & 0 deletions lib/uri.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
85 changes: 85 additions & 0 deletions lib/uri.h
Original file line number Diff line number Diff line change
@@ -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;
};

}
Loading