Skip to content

Commit 8947557

Browse files
authored
Merge pull request #407 from quotient-im/kitsune-resource-resolver
Matrix URIs and resolving them
2 parents 9f9577c + bd74588 commit 8947557

File tree

8 files changed

+724
-30
lines changed

8 files changed

+724
-30
lines changed

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ set(lib_SRCS
149149
lib/room.cpp
150150
lib/user.cpp
151151
lib/avatar.cpp
152+
lib/uri.cpp
153+
lib/uriresolver.cpp
152154
lib/syncdata.cpp
153155
lib/settings.cpp
154156
lib/networksettings.cpp

lib/quotient_common.h

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 };
1414

1515
Q_ENUM_NS(RunningPolicy)
1616

17-
enum ResourceResolveResult : short {
17+
enum UriResolveResult : short {
1818
StillResolving = -1,
19-
Resolved = 0,
20-
UnknownMatrixId,
21-
MalformedMatrixId,
22-
NoAccount,
23-
EmptyMatrixId
19+
UriResolved = 0,
20+
CouldNotResolve,
21+
IncorrectAction,
22+
InvalidUri,
23+
NoAccount
2424
};
25-
Q_ENUM_NS(ResourceResolveResult)
25+
Q_ENUM_NS(UriResolveResult)
2626

2727
} // namespace Quotient
2828
/// \deprecated Use namespace Quotient instead

lib/uri.cpp

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#include "uri.h"
2+
3+
#include "logging.h"
4+
5+
#include <QtCore/QRegularExpression>
6+
7+
using namespace Quotient;
8+
9+
struct ReplacePair { QByteArray uriString; char sigil; };
10+
/// Defines bi-directional mapping of path prefixes and sigils
11+
static const auto replacePairs = {
12+
ReplacePair { "user/", '@' },
13+
{ "roomid/", '!' },
14+
{ "room/", '#' },
15+
// The notation for bare event ids is not proposed in MSC2312 but there's
16+
// https://github.com/matrix-org/matrix-doc/pull/2644
17+
{ "event/", '$' }
18+
};
19+
20+
Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query)
21+
{
22+
if (primaryId.isEmpty())
23+
primaryType_ = Empty;
24+
else {
25+
setScheme("matrix");
26+
QString pathToBe;
27+
primaryType_ = Invalid;
28+
if (primaryId.size() < 2) // There should be something after sigil
29+
return;
30+
for (const auto& p: replacePairs)
31+
if (primaryId[0] == p.sigil) {
32+
primaryType_ = Type(p.sigil);
33+
pathToBe = p.uriString + primaryId.mid(1);
34+
break;
35+
}
36+
if (!secondaryId.isEmpty()) {
37+
if (secondaryId.size() < 2) {
38+
primaryType_ = Invalid;
39+
return;
40+
}
41+
pathToBe += "/event/" + secondaryId.mid(1);
42+
}
43+
setPath(pathToBe);
44+
}
45+
setQuery(std::move(query));
46+
}
47+
48+
Uri::Uri(QUrl url) : QUrl(std::move(url))
49+
{
50+
// NB: don't try to use `url` from here on, it's moved-from and empty
51+
if (isEmpty())
52+
return; // primaryType_ == Empty
53+
54+
if (!QUrl::isValid()) { // MatrixUri::isValid() checks primaryType_
55+
primaryType_ = Invalid;
56+
return;
57+
}
58+
59+
if (scheme() == "matrix") {
60+
// Check sanity as per https://github.com/matrix-org/matrix-doc/pull/2312
61+
const auto& urlPath = path();
62+
const auto& splitPath = urlPath.splitRef('/');
63+
switch (splitPath.size()) {
64+
case 2:
65+
break;
66+
case 4:
67+
if (splitPath[2] == "event")
68+
break;
69+
[[fallthrough]];
70+
default:
71+
return; // Invalid
72+
}
73+
74+
for (const auto& p: replacePairs)
75+
if (urlPath.startsWith(p.uriString)) {
76+
primaryType_ = Type(p.sigil);
77+
return; // The only valid return path for matrix: URIs
78+
}
79+
qCDebug(MAIN) << "The matrix: URI is not recognised:"
80+
<< toDisplayString();
81+
return;
82+
}
83+
84+
primaryType_ = NonMatrix; // Default, unless overridden by the code below
85+
if (scheme() == "https" && authority() == "matrix.to") {
86+
// See https://matrix.org/docs/spec/appendices#matrix-to-navigation
87+
static const QRegularExpression MatrixToUrlRE {
88+
R"(^/(?<main>[^/?]+)(/(?<sec>[^?]+))?(\?(?<query>.+))?$)"
89+
};
90+
// matrix.to accepts both literal sigils (as well as & and ? used in
91+
// its "query" substitute) and their %-encoded forms;
92+
// so force QUrl to decode everything.
93+
auto f = fragment(QUrl::FullyDecoded);
94+
if (auto&& m = MatrixToUrlRE.match(f); m.hasMatch())
95+
*this = Uri { m.captured("main").toUtf8(),
96+
m.captured("sec").toUtf8(), m.captured("query") };
97+
}
98+
}
99+
100+
Uri::Uri(const QString& uriOrId) : Uri(fromUserInput(uriOrId)) {}
101+
102+
Uri Uri::fromUserInput(const QString& uriOrId)
103+
{
104+
if (uriOrId.isEmpty())
105+
return {}; // type() == None
106+
107+
// A quick check if uriOrId is a plain Matrix id
108+
// Bare event ids cannot be resolved without a room scope as per the current
109+
// spec but there's a movement towards making them navigable (see, e.g.,
110+
// https://github.com/matrix-org/matrix-doc/pull/2644) - so treat them
111+
// as valid
112+
if (QStringLiteral("!@#+$").contains(uriOrId[0]))
113+
return Uri { uriOrId.toUtf8() };
114+
115+
return Uri { QUrl::fromUserInput(uriOrId) };
116+
}
117+
118+
Uri::Type Uri::type() const { return primaryType_; }
119+
120+
Uri::SecondaryType Uri::secondaryType() const
121+
{
122+
return path().section('/', 2, 2) == "event" ? EventId : NoSecondaryId;
123+
}
124+
125+
QUrl Uri::toUrl(UriForm form) const
126+
{
127+
if (!isValid())
128+
return {};
129+
130+
if (form == CanonicalUri || type() == NonMatrix)
131+
return *this;
132+
133+
QUrl url;
134+
url.setScheme("https");
135+
url.setHost("matrix.to");
136+
url.setPath("/");
137+
auto fragment = primaryId();
138+
if (const auto& secId = secondaryId(); !secId.isEmpty())
139+
fragment += '/' + secId;
140+
if (const auto& q = query(); !q.isEmpty())
141+
fragment += '?' + q;
142+
url.setFragment(fragment);
143+
return url;
144+
}
145+
146+
QString Uri::primaryId() const
147+
{
148+
if (primaryType_ == Empty || primaryType_ == Invalid)
149+
return {};
150+
151+
const auto& idStem = path().section('/', 1, 1);
152+
return idStem.isEmpty() ? idStem : primaryType_ + idStem;
153+
}
154+
155+
QString Uri::secondaryId() const
156+
{
157+
const auto& idStem = path().section('/', 3);
158+
return idStem.isEmpty() ? idStem : secondaryType() + idStem;
159+
}
160+
161+
static const auto ActionKey = QStringLiteral("action");
162+
163+
QString Uri::action() const
164+
{
165+
return type() == NonMatrix || !isValid()
166+
? QString()
167+
: QUrlQuery { query() }.queryItemValue(ActionKey);
168+
}
169+
170+
void Uri::setAction(const QString& newAction)
171+
{
172+
if (!isValid()) {
173+
qCWarning(MAIN) << "Cannot set an action on an invalid Quotient::Uri";
174+
return;
175+
}
176+
QUrlQuery q { query() };
177+
q.removeQueryItem(ActionKey);
178+
q.addQueryItem(ActionKey, newAction);
179+
setQuery(q);
180+
}
181+
182+
QStringList Uri::viaServers() const
183+
{
184+
return QUrlQuery { query() }.allQueryItemValues(QStringLiteral("via"),
185+
QUrl::EncodeReserved);
186+
}
187+
188+
bool Uri::isValid() const
189+
{
190+
return primaryType_ != Empty && primaryType_ != Invalid;
191+
}

lib/uri.h

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#pragma once
2+
3+
#include "quotient_common.h"
4+
5+
#include <QtCore/QUrl>
6+
#include <QtCore/QUrlQuery>
7+
8+
namespace Quotient {
9+
10+
/*! \brief A wrapper around a Matrix URI or identifier
11+
*
12+
* This class encapsulates a Matrix resource identifier, passed in either of
13+
* 3 forms: a plain Matrix identifier (sigil, localpart, serverpart or, for
14+
* modern event ids, sigil and base64 hash); an MSC2312 URI (aka matrix: URI);
15+
* or a matrix.to URL. The input can be either encoded (serverparts with
16+
* punycode, the rest with percent-encoding) or unencoded (in this case it is
17+
* the caller's responsibility to resolve all possible ambiguities).
18+
*
19+
* The class provides functions to check the validity of the identifier,
20+
* its type, and obtain components, also in either unencoded (for displaying)
21+
* or encoded (for APIs) form.
22+
*/
23+
class Uri : private QUrl {
24+
Q_GADGET
25+
public:
26+
enum Type : char {
27+
Invalid = char(-1),
28+
Empty = 0x0,
29+
UserId = '@',
30+
RoomId = '!',
31+
RoomAlias = '#',
32+
Group = '+',
33+
BareEventId = '$', // https://github.com/matrix-org/matrix-doc/pull/2644
34+
NonMatrix = ':'
35+
};
36+
Q_ENUM(Type)
37+
enum SecondaryType : char { NoSecondaryId = 0x0, EventId = '$' };
38+
Q_ENUM(SecondaryType)
39+
40+
enum UriForm : short { CanonicalUri, MatrixToUri };
41+
Q_ENUM(UriForm)
42+
43+
/// Construct an empty Matrix URI
44+
Uri() = default;
45+
/*! \brief Decode a user input string to a Matrix identifier
46+
*
47+
* Accepts plain Matrix ids, MSC2312 URIs (aka matrix: URIs) and
48+
* matrix.to URLs. In case of URIs/URLs, it uses QUrl's TolerantMode
49+
* parser to decode common mistakes/irregularities (see QUrl documentation
50+
* for more details).
51+
*/
52+
Uri(const QString& uriOrId);
53+
54+
/// Construct a Matrix URI from components
55+
explicit Uri(QByteArray primaryId, QByteArray secondaryId = {},
56+
QString query = {});
57+
/// Construct a Matrix URI from matrix.to or MSC2312 (matrix:) URI
58+
explicit Uri(QUrl url);
59+
60+
static Uri fromUserInput(const QString& uriOrId);
61+
static Uri fromUrl(QUrl url);
62+
63+
/// Get the primary type of the Matrix URI (user id, room id or alias)
64+
/*! Note that this does not include an event as a separate type, since
65+
* events can only be addressed inside of rooms, which, in turn, are
66+
* addressed either by id or alias. If you need to check whether the URI
67+
* is specifically an event URI, use secondaryType() instead.
68+
*/
69+
Q_INVOKABLE Type type() const;
70+
Q_INVOKABLE SecondaryType secondaryType() const;
71+
Q_INVOKABLE QUrl toUrl(UriForm form = CanonicalUri) const;
72+
Q_INVOKABLE QString primaryId() const;
73+
Q_INVOKABLE QString secondaryId() const;
74+
Q_INVOKABLE QString action() const;
75+
Q_INVOKABLE void setAction(const QString& newAction);
76+
Q_INVOKABLE QStringList viaServers() const;
77+
Q_INVOKABLE bool isValid() const;
78+
using QUrl::path, QUrl::query, QUrl::fragment;
79+
using QUrl::isEmpty, QUrl::toDisplayString;
80+
81+
private:
82+
Type primaryType_ = Empty;
83+
};
84+
85+
}

0 commit comments

Comments
 (0)