1// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net>
2// SPDX-License-Identifier: LGPL-2.1-or-later
3
4#include "uri.h"
5
6#include "logging_categories_p.h"
7#include "util.h"
8
9#include <QtCore/QRegularExpression>
10
11using namespace Quotient;
12
13namespace {
14
15struct ReplacePair { QLatin1String uriString; char sigil; };
16/// \brief Defines bi-directional mapping of path prefixes and sigils
17///
18/// When there are two prefixes for the same sigil, the first matching
19/// entry for a given sigil is used.
20const ReplacePair replacePairs[] = {
21 { .uriString: "u/"_ls, .sigil: '@' },
22 { .uriString: "user/"_ls, .sigil: '@' },
23 { .uriString: "roomid/"_ls, .sigil: '!' },
24 { .uriString: "r/"_ls, .sigil: '#' },
25 { .uriString: "room/"_ls, .sigil: '#' },
26 // The notation for bare event ids is not proposed in MSC2312 but there's
27 // https://github.com/matrix-org/matrix-doc/pull/2644
28 { .uriString: "e/"_ls, .sigil: '$' },
29 { .uriString: "event/"_ls, .sigil: '$' }
30};
31
32}
33
34Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query)
35{
36 if (primaryId.isEmpty())
37 primaryType_ = Empty;
38 else {
39 setScheme("matrix"_ls);
40 QString pathToBe;
41 primaryType_ = Invalid;
42 if (primaryId.size() < 2) // There should be something after sigil
43 return;
44 for (const auto& p: replacePairs)
45 if (primaryId[0] == p.sigil) {
46 primaryType_ = Type(p.sigil);
47 auto safePrimaryId = primaryId.mid(index: 1);
48 safePrimaryId.replace(before: '/', after: "%2F");
49 pathToBe = p.uriString + QString::fromUtf8(ba: safePrimaryId);
50 break;
51 }
52 if (!secondaryId.isEmpty()) {
53 if (secondaryId.size() < 2) {
54 primaryType_ = Invalid;
55 return;
56 }
57 auto safeSecondaryId = secondaryId.mid(index: 1);
58 safeSecondaryId.replace(before: '/', after: "%2F");
59 pathToBe += "/event/"_ls + QString::fromUtf8(ba: safeSecondaryId);
60 }
61 setPath(path: pathToBe, mode: QUrl::TolerantMode);
62 }
63 if (!query.isEmpty())
64 setQuery(query);
65}
66
67static inline auto encodedPath(const QUrl& url)
68{
69 return url.path(options: QUrl::EncodeDelimiters | QUrl::EncodeUnicode);
70}
71
72static QString pathSegment(const QUrl& url, int which)
73{
74 return QUrl::fromPercentEncoding(
75 encodedPath(url).section(asep: u'/', astart: which, aend: which).toUtf8());
76}
77
78static auto decodeFragmentPart(QStringView part)
79{
80 return QUrl::fromPercentEncoding(part.toLatin1()).toUtf8();
81}
82
83static inline auto matrixToUrlRegexInit()
84{
85 // See https://matrix.org/docs/spec/appendices#matrix-to-navigation
86 const QRegularExpression MatrixToUrlRE {
87 "^/(?<main>[^:]+(:|%3A|%3a)[^/?]+)(/(?<sec>(\\$|%24)[^?]+))?(\\?(?<query>.+))?$"_ls
88 };
89 Q_ASSERT(MatrixToUrlRE.isValid());
90 return MatrixToUrlRE;
91}
92
93Uri::Uri(QUrl url) : QUrl(std::move(url))
94{
95 // NB: don't try to use `url` from here on, it's moved-from and empty
96 if (isEmpty())
97 return; // primaryType_ == Empty
98
99 primaryType_ = Invalid;
100 if (!QUrl::isValid()) // MatrixUri::isValid() checks primaryType_
101 return;
102
103 if (scheme() == "matrix"_ls) {
104 // Check sanity as per https://github.com/matrix-org/matrix-doc/pull/2312
105 const auto& urlPath = encodedPath(url: *this);
106 const auto& splitPath = urlPath.split(sep: u'/');
107 switch (splitPath.size()) {
108 case 2:
109 break;
110 case 4:
111 if (splitPath[2] == "event"_ls || splitPath[2] == "e"_ls)
112 break;
113 [[fallthrough]];
114 default:
115 return; // Invalid
116 }
117
118 for (const auto& p: replacePairs)
119 if (urlPath.startsWith(s: p.uriString)) {
120 primaryType_ = Type(p.sigil);
121 return; // The only valid return path for matrix: URIs
122 }
123 qCDebug(MAIN) << "The matrix: URI is not recognised:"
124 << toDisplayString();
125 return;
126 }
127
128 primaryType_ = NonMatrix; // Default, unless overridden by the code below
129 if (scheme() == "https"_ls && authority() == "matrix.to"_ls) {
130 static const auto MatrixToUrlRE = matrixToUrlRegexInit();
131 // matrix.to accepts both literal sigils (as well as & and ? used in
132 // its "query" substitute) and their %-encoded forms;
133 // so force QUrl to decode everything.
134 auto f = fragment(options: QUrl::EncodeUnicode);
135 if (auto&& m = MatrixToUrlRE.match(subject: f); m.hasMatch())
136 *this = Uri { decodeFragmentPart(part: m.capturedView(name: u"main")),
137 decodeFragmentPart(part: m.capturedView(name: u"sec")),
138 QString::fromUtf8(ba: decodeFragmentPart(part: m.capturedView(name: u"query"))) };
139 }
140}
141
142Uri::Uri(const QString& uriOrId) : Uri(fromUserInput(uriOrId)) {}
143
144Uri Uri::fromUserInput(const QString& uriOrId)
145{
146 if (uriOrId.isEmpty())
147 return {}; // type() == None
148
149 // A quick check if uriOrId is a plain Matrix id
150 // Bare event ids cannot be resolved without a room scope as per the current
151 // spec but there's a movement towards making them navigable (see, e.g.,
152 // https://github.com/matrix-org/matrix-doc/pull/2644) - so treat them
153 // as valid
154 if (QStringLiteral("!@#+$").contains(c: uriOrId[0]))
155 return Uri { uriOrId.toUtf8() };
156
157 return Uri { QUrl::fromUserInput(userInput: uriOrId) };
158}
159
160Uri::Type Uri::type() const { return primaryType_; }
161
162Uri::SecondaryType Uri::secondaryType() const
163{
164 const auto& type2 = pathSegment(url: *this, which: 2);
165 return type2 == "event"_ls || type2 == "e"_ls ? EventId : NoSecondaryId;
166}
167
168QUrl Uri::toUrl(UriForm form) const
169{
170 if (!isValid())
171 return {};
172
173 if (form == CanonicalUri || type() == NonMatrix)
174 return SLICE(*this, QUrl);
175
176 QUrl url;
177 url.setScheme("https"_ls);
178 url.setHost(host: "matrix.to"_ls);
179 url.setPath(path: "/"_ls);
180 auto fragment = u'/' + primaryId();
181 if (const auto& secId = secondaryId(); !secId.isEmpty())
182 fragment += u'/' + secId;
183 if (const auto& q = query(); !q.isEmpty())
184 fragment += u'?' + q;
185 url.setFragment(fragment);
186 return url;
187}
188
189QString Uri::primaryId() const
190{
191 if (primaryType_ == Empty || primaryType_ == Invalid)
192 return {};
193
194 auto idStem = pathSegment(url: *this, which: 1);
195 if (!idStem.isEmpty())
196 idStem.push_front(c: QChar::fromLatin1(c: primaryType_));
197 return idStem;
198}
199
200QString Uri::secondaryId() const
201{
202 auto idStem = pathSegment(url: *this, which: 3);
203 if (!idStem.isEmpty())
204 idStem.push_front(c: QChar::fromLatin1(c: secondaryType()));
205 return idStem;
206}
207
208static const auto ActionKey = QStringLiteral("action");
209
210QString Uri::action() const
211{
212 return type() == NonMatrix || !isValid()
213 ? QString()
214 : QUrlQuery { query() }.queryItemValue(key: ActionKey);
215}
216
217void Uri::setAction(const QString& newAction)
218{
219 if (!isValid()) {
220 qCWarning(MAIN) << "Cannot set an action on an invalid Quotient::Uri";
221 return;
222 }
223 QUrlQuery q { query() };
224 q.removeQueryItem(key: ActionKey);
225 q.addQueryItem(key: ActionKey, value: newAction);
226 setQuery(q);
227}
228
229QStringList Uri::viaServers() const
230{
231 return QUrlQuery { query() }.allQueryItemValues(QStringLiteral("via"),
232 encoding: QUrl::EncodeReserved);
233}
234
235bool Uri::isValid() const
236{
237 return primaryType_ != Empty && primaryType_ != Invalid;
238}
239