From baccd0aa42c155f293fb4d75f1bf81c681304ae6 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sun, 27 Dec 2020 11:46:53 +0100 Subject: [PATCH] fix: Properly handle url encoding in matrix.to URLs --- .../matrix_identifier_string_extension.dart | 25 ++-- lib/utils/url_launcher.dart | 124 ++++++++++-------- ...trix_identifier_string_extension_test.dart | 34 +++-- 3 files changed, 107 insertions(+), 76 deletions(-) diff --git a/lib/utils/matrix_identifier_string_extension.dart b/lib/utils/matrix_identifier_string_extension.dart index 140be067..846fb8b9 100644 --- a/lib/utils/matrix_identifier_string_extension.dart +++ b/lib/utils/matrix_identifier_string_extension.dart @@ -1,24 +1,33 @@ +import '../app_config.dart'; + extension MatrixIdentifierStringExtension on String { /// Separates room identifiers with an event id and possibly a query parameter into its components. MatrixIdentifierStringExtensionResults parseIdentifierIntoParts() { - final match = RegExp(r'^([#!][^:]*:[^\/?]*)(?:\/(\$[^?]*))?(?:\?(.*))?$') - .firstMatch(this); + final isUrl = startsWith(AppConfig.inviteLinkPrefix); + var s = this; + if (isUrl) { + // as we decode a component we may only call it on the url part *before* the "query" part + final parts = replaceFirst(AppConfig.inviteLinkPrefix, '').split('?'); + s = Uri.decodeComponent(parts.removeAt(0)) + '?' + parts.join('?'); + } + final match = RegExp(r'^([#!@+][^:]*:[^\/?]*)(?:\/(\$[^?]*))?(?:\?(.*))?$') + .firstMatch(s); if (match == null) { return null; } return MatrixIdentifierStringExtensionResults( - roomIdOrAlias: match.group(1), - eventId: match.group(2), - queryString: match.group(3), + primaryIdentifier: match.group(1), + secondaryIdentifier: match.group(2), + queryString: match.group(3)?.isNotEmpty ?? false ? match.group(3) : null, ); } } class MatrixIdentifierStringExtensionResults { - final String roomIdOrAlias; - final String eventId; + final String primaryIdentifier; + final String secondaryIdentifier; final String queryString; MatrixIdentifierStringExtensionResults( - {this.roomIdOrAlias, this.eventId, this.queryString}); + {this.primaryIdentifier, this.secondaryIdentifier, this.queryString}); } diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index 898a8fab..a915237e 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -25,16 +25,18 @@ class UrlLauncher { void openMatrixToUrl() async { final matrix = Matrix.of(context); - final identifier = url.replaceAll(AppConfig.inviteLinkPrefix, ''); - if (identifier[0] == '#' || identifier[0] == '!') { - // sometimes we have identifiers which have an event id and additional query parameters - // we want to separate those. - final identityParts = identifier.parseIdentifierIntoParts(); - if (identityParts == null) { - return; // no match, nothing to do - } - final roomIdOrAlias = identityParts.roomIdOrAlias; - final event = identityParts.eventId; + // The identifier might be a matrix.to url and needs escaping. Or, it might have multiple + // identifiers (room id & event id), or it might also have a query part. + // All this needs parsing. + final identityParts = url.parseIdentifierIntoParts(); + if (identityParts == null) { + return; // no match, nothing to do + } + if (identityParts.primaryIdentifier.sigil == '#' || + identityParts.primaryIdentifier.sigil == '!') { + // we got a room! Let's open that one + final roomIdOrAlias = identityParts.primaryIdentifier; + final event = identityParts.secondaryIdentifier; final query = identityParts.queryString; var room = matrix.client.getRoomByAlias(roomIdOrAlias) ?? matrix.client.getRoomById(roomIdOrAlias); @@ -42,7 +44,7 @@ class UrlLauncher { // we make the servers a set and later on convert to a list, so that we can easily // deduplicate servers added via alias lookup and query parameter var servers = {}; - if (room == null && roomIdOrAlias.startsWith('#')) { + if (room == null && roomIdOrAlias.sigil == '#') { // we were unable to find the room locally...so resolve it final response = await showFutureLoadingDialog( context: context, @@ -81,67 +83,73 @@ class UrlLauncher { return; } if (roomIdOrAlias.sigil == '!') { - roomId = roomIdOrAlias; - final response = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.joinRoomOrAlias( - roomIdOrAlias, - servers: servers.isNotEmpty ? servers.toList() : null, - ), - ); - if (response.error != null) return; - // wait for two seconds so that it probably came down /sync - await showFutureLoadingDialog( + if (await showOkCancelAlertDialog( + context: context, + title: 'Join room $roomIdOrAlias', + ) == + OkCancelResult.ok) { + roomId = roomIdOrAlias; + final response = await showFutureLoadingDialog( context: context, - future: () => Future.delayed(const Duration(seconds: 2))); - await Navigator.pushAndRemoveUntil( - context, - AppRoute.defaultRoute( - context, ChatView(response.result, scrollToEventId: event)), - (r) => r.isFirst, - ); - } else if (identifier.sigil == '#') { + future: () => matrix.client.joinRoomOrAlias( + roomIdOrAlias, + servers: servers.isNotEmpty ? servers.toList() : null, + ), + ); + if (response.error != null) return; + // wait for two seconds so that it probably came down /sync + await showFutureLoadingDialog( + context: context, + future: () => Future.delayed(const Duration(seconds: 2))); + await Navigator.pushAndRemoveUntil( + context, + AppRoute.defaultRoute( + context, ChatView(response.result, scrollToEventId: event)), + (r) => r.isFirst, + ); + } + } else { await Navigator.of(context).pushAndRemoveUntil( AppRoute.defaultRoute( context, - DiscoverView(alias: identifier), + DiscoverView(alias: roomIdOrAlias), ), (r) => r.isFirst, ); - } else if (identifier.sigil == '@') { - final user = User( - identifier, - room: Room(id: '', client: matrix.client), + } + } else if (identityParts.primaryIdentifier.sigil == '@') { + final user = User( + identityParts.primaryIdentifier, + room: Room(id: '', client: matrix.client), + ); + var roomId = matrix.client.getDirectChatFromUserId(user.id); + if (roomId != null) { + await Navigator.pushAndRemoveUntil( + context, + AppRoute.defaultRoute(context, ChatView(roomId)), + (r) => r.isFirst, ); - var roomId = matrix.client.getDirectChatFromUserId(identifier); + return; + } + + if (await showOkCancelAlertDialog( + context: context, + title: 'Message user ${user.id}', + ) == + OkCancelResult.ok) { + roomId = (await showFutureLoadingDialog( + context: context, + future: () => user.startDirectChat(), + )) + .result; + Navigator.of(context).pop(); + if (roomId != null) { await Navigator.pushAndRemoveUntil( context, AppRoute.defaultRoute(context, ChatView(roomId)), (r) => r.isFirst, ); - return; - } - - if (await showOkCancelAlertDialog( - context: context, - title: 'Message user $identifier', - ) == - OkCancelResult.ok) { - roomId = (await showFutureLoadingDialog( - context: context, - future: () => user.startDirectChat(), - )) - .result; - Navigator.of(context).pop(); - - if (roomId != null) { - await Navigator.pushAndRemoveUntil( - context, - AppRoute.defaultRoute(context, ChatView(roomId)), - (r) => r.isFirst, - ); - } } } } diff --git a/test/matrix_identifier_string_extension_test.dart b/test/matrix_identifier_string_extension_test.dart index 06db66d7..d2095689 100644 --- a/test/matrix_identifier_string_extension_test.dart +++ b/test/matrix_identifier_string_extension_test.dart @@ -5,27 +5,41 @@ void main() { group('Matrix Identifier String Extension', () { test('parseIdentifierIntoParts', () { var res = '#alias:beep'.parseIdentifierIntoParts(); - expect(res.roomIdOrAlias, '#alias:beep'); - expect(res.eventId, null); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, null); expect(res.queryString, null); res = 'blha'.parseIdentifierIntoParts(); expect(res, null); res = '#alias:beep/\$event'.parseIdentifierIntoParts(); - expect(res.roomIdOrAlias, '#alias:beep'); - expect(res.eventId, '\$event'); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, '\$event'); expect(res.queryString, null); res = '#alias:beep?blubb'.parseIdentifierIntoParts(); - expect(res.roomIdOrAlias, '#alias:beep'); - expect(res.eventId, null); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, null); expect(res.queryString, 'blubb'); res = '#alias:beep/\$event?blubb'.parseIdentifierIntoParts(); - expect(res.roomIdOrAlias, '#alias:beep'); - expect(res.eventId, '\$event'); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, '\$event'); expect(res.queryString, 'blubb'); res = '#/\$?:beep/\$event?blubb?b'.parseIdentifierIntoParts(); - expect(res.roomIdOrAlias, '#/\$?:beep'); - expect(res.eventId, '\$event'); + expect(res.primaryIdentifier, '#/\$?:beep'); + expect(res.secondaryIdentifier, '\$event'); expect(res.queryString, 'blubb?b'); + + res = 'https://matrix.to/#/#alias:beep'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, null); + expect(res.queryString, null); + res = 'https://matrix.to/#/%23alias%3abeep'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, null); + expect(res.queryString, null); + res = 'https://matrix.to/#/%23alias%3abeep?boop%F0%9F%A7%A1%F0%9F%A6%8A' + .parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, null); + expect(res.queryString, 'boop%F0%9F%A7%A1%F0%9F%A6%8A'); }); }); }