Line data Source code
1 : /*
2 : * Famedly Matrix SDK
3 : * Copyright (C) 2020, 2021 Famedly GmbH
4 : *
5 : * This program is free software: you can redistribute it and/or modify
6 : * it under the terms of the GNU Affero General Public License as
7 : * published by the Free Software Foundation, either version 3 of the
8 : * License, or (at your option) any later version.
9 : *
10 : * This program is distributed in the hope that it will be useful,
11 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : * GNU Affero General Public License for more details.
14 : *
15 : * You should have received a copy of the GNU Affero General Public License
16 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 : */
18 :
19 : const Set<String> validSigils = {'@', '!', '#', '\$', '+'};
20 :
21 : const int maxLength = 255;
22 :
23 : extension MatrixIdExtension on String {
24 42 : List<String> _getParts() {
25 42 : final s = substring(1);
26 42 : final ix = s.indexOf(':');
27 84 : if (ix == -1) {
28 8 : return [substring(1)];
29 : }
30 168 : return [s.substring(0, ix), s.substring(ix + 1)];
31 : }
32 :
33 42 : bool get isValidMatrixId {
34 42 : if (isEmpty) return false;
35 84 : if (length > maxLength) return false;
36 42 : final sigil = substring(0, 1);
37 42 : if (!validSigils.contains(sigil)) {
38 : return false;
39 : }
40 : // event IDs and room IDs do not have to have a domain
41 126 : if ({'\$', '!'}.contains(sigil)) {
42 : return true;
43 : }
44 : // all other matrix IDs have to have a domain
45 42 : final parts = _getParts();
46 : // the localpart can be an empty string, e.g. for aliases
47 168 : if (parts.length != 2 || parts[1].isEmpty) {
48 : return false;
49 : }
50 : return true;
51 : }
52 :
53 6 : String? get sigil => isValidMatrixId ? substring(0, 1) : null;
54 :
55 168 : String? get localpart => isValidMatrixId ? _getParts().first : null;
56 :
57 168 : String? get domain => isValidMatrixId ? _getParts().last : null;
58 :
59 8 : bool equals(String? other) => toLowerCase() == other?.toLowerCase();
60 :
61 : /// Parse a matrix identifier string into a Uri. Primary and secondary identifiers
62 : /// are stored in pathSegments. The query string is stored as such.
63 2 : Uri? _parseIdentifierIntoUri() {
64 : const matrixUriPrefix = 'matrix:';
65 : const matrixToPrefix = 'https://matrix.to/#/';
66 4 : if (toLowerCase().startsWith(matrixUriPrefix)) {
67 2 : final uri = Uri.tryParse(this);
68 : if (uri == null) return null;
69 2 : final pathSegments = uri.pathSegments;
70 2 : final identifiers = <String>[];
71 8 : for (var i = 0; i < pathSegments.length - 1; i += 2) {
72 2 : final thisSigil = {
73 : 'u': '@',
74 : 'roomid': '!',
75 : 'r': '#',
76 : 'e': '\$',
77 6 : }[pathSegments[i].toLowerCase()];
78 : if (thisSigil == null) {
79 : break;
80 : }
81 8 : identifiers.add(thisSigil + pathSegments[i + 1]);
82 : }
83 2 : return uri.replace(pathSegments: identifiers);
84 4 : } else if (toLowerCase().startsWith(matrixToPrefix)) {
85 2 : return Uri.tryParse(
86 30 : '//${substring(matrixToPrefix.length - 1).replaceAllMapped(RegExp(r'(?<=/)[#!@+][^:]*:|(\?.*$)'), (m) => m[0]!.replaceAllMapped(RegExp(m.group(1) != null ? '' : '[/?]'), (m) => Uri.encodeComponent(m.group(0)!))).replaceAll('#', '%23')}',
87 : );
88 : } else {
89 2 : return Uri(
90 2 : pathSegments: RegExp(r'/((?:[#!@+][^:]*:)?[^/?]*)(?:\?.*$)?')
91 4 : .allMatches('/$this')
92 6 : .map((m) => m[1]!),
93 2 : query: RegExp(r'(?:/(?:[#!@+][^:]*:)?[^/?]*)*\?(.*$)')
94 6 : .firstMatch('/$this')?[1],
95 : );
96 : }
97 : }
98 :
99 : /// Separate a matrix identifier string into a primary indentifier, a secondary identifier,
100 : /// a query string and already parsed `via` parameters. A matrix identifier string
101 : /// can be an mxid, a matrix.to-url or a matrix-uri.
102 2 : MatrixIdentifierStringExtensionResults? parseIdentifierIntoParts() {
103 2 : final uri = _parseIdentifierIntoUri();
104 : if (uri == null) return null;
105 8 : final primary = uri.pathSegments.isNotEmpty ? uri.pathSegments[0] : null;
106 2 : if (primary == null || !primary.isValidMatrixId) return null;
107 10 : final secondary = uri.pathSegments.length > 1 ? uri.pathSegments[1] : null;
108 2 : if (secondary != null && !secondary.isValidMatrixId) return null;
109 :
110 2 : return MatrixIdentifierStringExtensionResults(
111 : primaryIdentifier: primary,
112 : secondaryIdentifier: secondary,
113 6 : queryString: uri.query.isNotEmpty ? uri.query : null,
114 8 : via: (uri.queryParametersAll['via'] ?? []).toSet(),
115 4 : action: uri.queryParameters['action'],
116 : );
117 : }
118 : }
119 :
120 : class MatrixIdentifierStringExtensionResults {
121 : final String primaryIdentifier;
122 : final String? secondaryIdentifier;
123 : final String? queryString;
124 : final Set<String> via;
125 : final String? action;
126 :
127 2 : MatrixIdentifierStringExtensionResults({
128 : required this.primaryIdentifier,
129 : this.secondaryIdentifier,
130 : this.queryString,
131 : this.via = const {},
132 : this.action,
133 : });
134 : }
|