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 | |
11 | using namespace Quotient; |
12 | |
13 | namespace { |
14 | |
15 | struct 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. |
20 | const 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 | |
34 | Uri::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 | |
67 | static inline auto encodedPath(const QUrl& url) |
68 | { |
69 | return url.path(options: QUrl::EncodeDelimiters | QUrl::EncodeUnicode); |
70 | } |
71 | |
72 | static 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 | |
78 | static auto decodeFragmentPart(QStringView part) |
79 | { |
80 | return QUrl::fromPercentEncoding(part.toLatin1()).toUtf8(); |
81 | } |
82 | |
83 | static 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 | |
93 | Uri::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 | |
142 | Uri::Uri(const QString& uriOrId) : Uri(fromUserInput(uriOrId)) {} |
143 | |
144 | Uri 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 | |
160 | Uri::Type Uri::type() const { return primaryType_; } |
161 | |
162 | Uri::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 | |
168 | QUrl 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 | |
189 | QString 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 | |
200 | QString 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 | |
208 | static const auto ActionKey = QStringLiteral("action" ); |
209 | |
210 | QString Uri::action() const |
211 | { |
212 | return type() == NonMatrix || !isValid() |
213 | ? QString() |
214 | : QUrlQuery { query() }.queryItemValue(key: ActionKey); |
215 | } |
216 | |
217 | void 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 | |
229 | QStringList Uri::viaServers() const |
230 | { |
231 | return QUrlQuery { query() }.allQueryItemValues(QStringLiteral("via" ), |
232 | encoding: QUrl::EncodeReserved); |
233 | } |
234 | |
235 | bool Uri::isValid() const |
236 | { |
237 | return primaryType_ != Empty && primaryType_ != Invalid; |
238 | } |
239 | |