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 36 : List<String> _getParts() { 25 36 : final s = substring(1); 26 36 : final ix = s.indexOf(':'); 27 72 : if (ix == -1) { 28 6 : return [substring(1)]; 29 : } 30 144 : return [s.substring(0, ix), s.substring(ix + 1)]; 31 : } 32 : 33 36 : bool get isValidMatrixId { 34 36 : if (isEmpty) return false; 35 72 : if (length > maxLength) return false; 36 72 : if (!validSigils.contains(substring(0, 1))) { 37 : return false; 38 : } 39 : // event IDs do not have to have a domain 40 72 : if (substring(0, 1) == '\$') { 41 : return true; 42 : } 43 : // all other matrix IDs have to have a domain 44 36 : final parts = _getParts(); 45 : // the localpart can be an empty string, e.g. for aliases 46 144 : if (parts.length != 2 || parts[1].isEmpty) { 47 : return false; 48 : } 49 : return true; 50 : } 51 : 52 9 : String? get sigil => isValidMatrixId ? substring(0, 1) : null; 53 : 54 144 : String? get localpart => isValidMatrixId ? _getParts().first : null; 55 : 56 136 : 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 : }