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 35 : List<String> _getParts() {
25 35 : final s = substring(1);
26 35 : final ix = s.indexOf(':');
27 70 : if (ix == -1) {
28 4 : return [substring(1)];
29 : }
30 140 : return [s.substring(0, ix), s.substring(ix + 1)];
31 : }
32 :
33 35 : bool get isValidMatrixId {
34 35 : if (isEmpty) return false;
35 70 : if (length > maxLength) return false;
36 70 : if (!validSigils.contains(substring(0, 1))) {
37 : return false;
38 : }
39 : // event IDs do not have to have a domain
40 70 : if (substring(0, 1) == '\$') {
41 : return true;
42 : }
43 : // all other matrix IDs have to have a domain
44 35 : final parts = _getParts();
45 : // the localpart can be an empty string, e.g. for aliases
46 140 : if (parts.length != 2 || parts[1].isEmpty) {
47 : return false;
48 : }
49 : return true;
50 : }
51 :
52 6 : String? get sigil => isValidMatrixId ? substring(0, 1) : null;
53 :
54 140 : String? get localpart => isValidMatrixId ? _getParts().first : null;
55 :
56 132 : String? get domain => isValidMatrixId ? _getParts().last : null;
57 :
58 8 : bool equals(String? other) => toLowerCase() == other?.toLowerCase();
59 :
60 : /// Parse a matrix identifier string into a Uri. Primary and secondary identifiers
61 : /// are stored in pathSegments. The query string is stored as such.
62 2 : Uri? _parseIdentifierIntoUri() {
63 : const matrixUriPrefix = 'matrix:';
64 : const matrixToPrefix = 'https://matrix.to/#/';
65 4 : if (toLowerCase().startsWith(matrixUriPrefix)) {
66 2 : final uri = Uri.tryParse(this);
67 : if (uri == null) return null;
68 2 : final pathSegments = uri.pathSegments;
69 2 : final identifiers = <String>[];
70 8 : for (var i = 0; i < pathSegments.length - 1; i += 2) {
71 2 : final thisSigil = {
72 : 'u': '@',
73 : 'roomid': '!',
74 : 'r': '#',
75 : 'e': '\$',
76 6 : }[pathSegments[i].toLowerCase()];
77 : if (thisSigil == null) {
78 : break;
79 : }
80 8 : identifiers.add(thisSigil + pathSegments[i + 1]);
81 : }
82 2 : return uri.replace(pathSegments: identifiers);
83 4 : } else if (toLowerCase().startsWith(matrixToPrefix)) {
84 2 : return Uri.tryParse(
85 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')}',
86 : );
87 : } else {
88 2 : return Uri(
89 2 : pathSegments: RegExp(r'/((?:[#!@+][^:]*:)?[^/?]*)(?:\?.*$)?')
90 4 : .allMatches('/$this')
91 6 : .map((m) => m[1]!),
92 2 : query: RegExp(r'(?:/(?:[#!@+][^:]*:)?[^/?]*)*\?(.*$)')
93 6 : .firstMatch('/$this')?[1],
94 : );
95 : }
96 : }
97 :
98 : /// Separate a matrix identifier string into a primary indentifier, a secondary identifier,
99 : /// a query string and already parsed `via` parameters. A matrix identifier string
100 : /// can be an mxid, a matrix.to-url or a matrix-uri.
101 2 : MatrixIdentifierStringExtensionResults? parseIdentifierIntoParts() {
102 2 : final uri = _parseIdentifierIntoUri();
103 : if (uri == null) return null;
104 8 : final primary = uri.pathSegments.isNotEmpty ? uri.pathSegments[0] : null;
105 2 : if (primary == null || !primary.isValidMatrixId) return null;
106 10 : final secondary = uri.pathSegments.length > 1 ? uri.pathSegments[1] : null;
107 2 : if (secondary != null && !secondary.isValidMatrixId) return null;
108 :
109 2 : return MatrixIdentifierStringExtensionResults(
110 : primaryIdentifier: primary,
111 : secondaryIdentifier: secondary,
112 6 : queryString: uri.query.isNotEmpty ? uri.query : null,
113 8 : via: (uri.queryParametersAll['via'] ?? []).toSet(),
114 4 : action: uri.queryParameters['action'],
115 : );
116 : }
117 : }
118 :
119 : class MatrixIdentifierStringExtensionResults {
120 : final String primaryIdentifier;
121 : final String? secondaryIdentifier;
122 : final String? queryString;
123 : final Set<String> via;
124 : final String? action;
125 :
126 2 : MatrixIdentifierStringExtensionResults({
127 : required this.primaryIdentifier,
128 : this.secondaryIdentifier,
129 : this.queryString,
130 : this.via = const {},
131 : this.action,
132 : });
133 : }
|