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 6 : 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 9 : 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 : } else { 87 2 : return Uri( 88 2 : pathSegments: RegExp(r'/((?:[#!@+][^:]*:)?[^/?]*)(?:\?.*$)?') 89 4 : .allMatches('/$this') 90 6 : .map((m) => m[1]!), 91 2 : query: RegExp(r'(?:/(?:[#!@+][^:]*:)?[^/?]*)*\?(.*$)') 92 6 : .firstMatch('/$this')?[1]); 93 : } 94 : } 95 : 96 : /// Separate a matrix identifier string into a primary indentifier, a secondary identifier, 97 : /// a query string and already parsed `via` parameters. A matrix identifier string 98 : /// can be an mxid, a matrix.to-url or a matrix-uri. 99 2 : MatrixIdentifierStringExtensionResults? parseIdentifierIntoParts() { 100 2 : final uri = _parseIdentifierIntoUri(); 101 : if (uri == null) return null; 102 8 : final primary = uri.pathSegments.isNotEmpty ? uri.pathSegments[0] : null; 103 2 : if (primary == null || !primary.isValidMatrixId) return null; 104 10 : final secondary = uri.pathSegments.length > 1 ? uri.pathSegments[1] : null; 105 2 : if (secondary != null && !secondary.isValidMatrixId) return null; 106 : 107 2 : return MatrixIdentifierStringExtensionResults( 108 : primaryIdentifier: primary, 109 : secondaryIdentifier: secondary, 110 6 : queryString: uri.query.isNotEmpty ? uri.query : null, 111 8 : via: (uri.queryParametersAll['via'] ?? []).toSet(), 112 4 : action: uri.queryParameters['action'], 113 : ); 114 : } 115 : } 116 : 117 : class MatrixIdentifierStringExtensionResults { 118 : final String primaryIdentifier; 119 : final String? secondaryIdentifier; 120 : final String? queryString; 121 : final Set<String> via; 122 : final String? action; 123 : 124 2 : MatrixIdentifierStringExtensionResults( 125 : {required this.primaryIdentifier, 126 : this.secondaryIdentifier, 127 : this.queryString, 128 : this.via = const {}, 129 : this.action}); 130 : }