diff --git a/lib/api/core.dart b/lib/api/core.dart index a3464810c7..fbbdb705ac 100644 --- a/lib/api/core.dart +++ b/lib/api/core.dart @@ -4,9 +4,10 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; class Auth { - const Auth({required this.realmUrl, required this.email, required this.apiKey}); + Auth({required this.realmUrl, required this.email, required this.apiKey}) + : assert(realmUrl.query.isEmpty && realmUrl.fragment.isEmpty); - final String realmUrl; + final Uri realmUrl; final String email; final String apiKey; } @@ -69,14 +70,8 @@ class LiveApiConnection extends ApiConnection { @override Future get(String route, Map? params) async { assert(_isOpen); - final baseUrl = Uri.parse(auth.realmUrl); - final url = Uri( - scheme: baseUrl.scheme, - userInfo: baseUrl.userInfo, - host: baseUrl.host, - port: baseUrl.port, - path: "/api/v1/$route", - queryParameters: encodeParameters(params)); + final url = auth.realmUrl.replace( + path: "/api/v1/$route", queryParameters: encodeParameters(params)); if (kDebugMode) print("GET $url"); final response = await _client.get(url, headers: _headers()); if (response.statusCode != 200) { @@ -89,7 +84,7 @@ class LiveApiConnection extends ApiConnection { Future post(String route, Map? params) async { assert(_isOpen); final response = await _client.post( - Uri.parse("${auth.realmUrl}/api/v1/$route"), + auth.realmUrl.replace(path: "/api/v1/$route"), headers: _headers(), body: encodeParameters(params)); if (response.statusCode != 200) { diff --git a/lib/api/route/account.dart b/lib/api/route/account.dart index d8949edfa2..219a7ee282 100644 --- a/lib/api/route/account.dart +++ b/lib/api/route/account.dart @@ -9,13 +9,13 @@ part 'account.g.dart'; /// https://zulip.com/api/fetch-api-key Future fetchApiKey({ - required String realmUrl, + required Uri realmUrl, required String username, required String password, }) async { // TODO dedupe this part with LiveApiConnection; make this function testable final response = await http.post( - Uri.parse("$realmUrl/api/v1/fetch_api_key"), + realmUrl.replace(path: "/api/v1/fetch_api_key"), body: encodeParameters({ 'username': RawParameter(username), 'password': RawParameter(password), diff --git a/lib/api/route/messages.dart b/lib/api/route/messages.dart index 0fc4db27fe..fd7c1811b3 100644 --- a/lib/api/route/messages.dart +++ b/lib/api/route/messages.dart @@ -69,7 +69,7 @@ Future sendMessage( }) async { // assert() is less verbose but would have no effect in production, I think: // https://dart.dev/guides/language/language-tour#assert - if (Uri.parse(connection.auth.realmUrl).origin != 'https://chat.zulip.org') { + if (connection.auth.realmUrl.origin != 'https://chat.zulip.org') { throw Exception('This binding can currently only be used on https://chat.zulip.org.'); } diff --git a/lib/api/route/realm.dart b/lib/api/route/realm.dart index 9ea99aaaf7..9febf4b536 100644 --- a/lib/api/route/realm.dart +++ b/lib/api/route/realm.dart @@ -17,11 +17,11 @@ part 'realm.g.dart'; // See thread, and the zulip-mobile code and chat thread it links to: // https://github.com/zulip/zulip-flutter/pull/55#discussion_r1160267577 Future getServerSettings({ - required String realmUrl, + required Uri realmUrl, }) async { // TODO dedupe this part with LiveApiConnection; make this function testable final response = await http.get( - Uri.parse("$realmUrl/api/v1/server_settings")); + realmUrl.replace(path: "/api/v1/server_settings")); if (response.statusCode != 200) { throw Exception('error on GET server_settings: status ${response.statusCode}'); } @@ -45,7 +45,7 @@ class GetServerSettingsResult { final bool emailAuthEnabled; final bool requireEmailFormatUsernames; - final String realmUri; + final Uri realmUri; final String realmName; final String realmIcon; final String realmDescription; diff --git a/lib/api/route/realm.g.dart b/lib/api/route/realm.g.dart index f085f61d9c..a961e03335 100644 --- a/lib/api/route/realm.g.dart +++ b/lib/api/route/realm.g.dart @@ -19,7 +19,7 @@ GetServerSettingsResult _$GetServerSettingsResultFromJson( emailAuthEnabled: json['email_auth_enabled'] as bool, requireEmailFormatUsernames: json['require_email_format_usernames'] as bool, - realmUri: json['realm_uri'] as String, + realmUri: Uri.parse(json['realm_uri'] as String), realmName: json['realm_name'] as String, realmIcon: json['realm_icon'] as String, realmDescription: json['realm_description'] as String, @@ -38,7 +38,7 @@ Map _$GetServerSettingsResultToJson( 'is_incompatible': instance.isIncompatible, 'email_auth_enabled': instance.emailAuthEnabled, 'require_email_format_usernames': instance.requireEmailFormatUsernames, - 'realm_uri': instance.realmUri, + 'realm_uri': instance.realmUri.toString(), 'realm_name': instance.realmName, 'realm_icon': instance.realmIcon, 'realm_description': instance.realmDescription, diff --git a/lib/model/store.dart b/lib/model/store.dart index 67a1a815dd..394c2da670 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -204,7 +204,7 @@ class PerAccountStore extends ChangeNotifier { @immutable class Account extends Auth { - const Account({ + Account({ required super.realmUrl, required super.email, required super.apiKey, @@ -243,8 +243,8 @@ class LiveGlobalStore extends GlobalStore { /// /// See "Server credentials" in the project README for how to fill in the /// `credential_fixture.dart` file this requires. -const Account _fixtureAccount = Account( - realmUrl: credentials.realmUrl, +final Account _fixtureAccount = Account( + realmUrl: Uri.parse(credentials.realmUrl), email: credentials.email, apiKey: credentials.apiKey, userId: credentials.userId, diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index 782854c704..533d3df8d8 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -65,7 +65,7 @@ class ChooseAccountPage extends StatelessWidget { for (final (:accountId, :account) in globalStore.accountEntries) _buildAccountItem(context, accountId: accountId, - title: Text(account.realmUrl), + title: Text(account.realmUrl.toString()), subtitle: Text(account.email)), const SizedBox(height: 12), ElevatedButton( @@ -103,7 +103,7 @@ class HomePage extends StatelessWidget { const SizedBox(height: 12), Text.rich(TextSpan( text: 'Connected to: ', - children: [bold(store.account.realmUrl)])), + children: [bold(store.account.realmUrl.toString())])), Text.rich(TextSpan( text: 'Zulip server version: ', children: [bold(store.zulipVersion)])), diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index ed45b9ec3a..0308a5726e 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -591,7 +591,7 @@ class RealmContentNetworkImage extends StatelessWidget { isAntiAlias: isAntiAlias, // Only send the auth header to the server `auth` belongs to. - headers: parsedSrc.origin == Uri.parse(auth.realmUrl).origin + headers: parsedSrc.origin == auth.realmUrl.origin ? authHeader(auth) : null, @@ -609,7 +609,7 @@ class RealmContentNetworkImage extends StatelessWidget { // This may dissolve when we start passing around URLs as [Uri] objects instead // of strings. String resolveUrl(String url, Account account) { - final realmUrl = Uri.parse(account.realmUrl); // TODO clean this up + final realmUrl = account.realmUrl; final resolved = realmUrl.resolve(url); // TODO handle if fails to parse return resolved.toString(); } diff --git a/lib/widgets/login.dart b/lib/widgets/login.dart index 31d175d3b6..5deddd6a02 100644 --- a/lib/widgets/login.dart +++ b/lib/widgets/login.dart @@ -47,7 +47,7 @@ class _AddAccountPageState extends State { } // TODO(#35): show feedback that we're working, while fetching server settings - final serverSettings = await getServerSettings(realmUrl: url.toString()); + final serverSettings = await getServerSettings(realmUrl: url); if (context.mounted) {} // https://github.com/dart-lang/linter/issues/4007 else { return; @@ -105,7 +105,7 @@ class _EmailPasswordLoginPageState extends State { Future _getUserId(FetchApiKeyResult fetchApiKeyResult) async { final FetchApiKeyResult(:email, :apiKey) = fetchApiKeyResult; final auth = Auth( - realmUrl: widget.realmUrl.toString(), email: email, apiKey: apiKey); + realmUrl: widget.realmUrl, email: email, apiKey: apiKey); final connection = LiveApiConnection(auth: auth); // TODO make this widget testable return (await getOwnUser(connection)).userId; } @@ -124,7 +124,7 @@ class _EmailPasswordLoginPageState extends State { final FetchApiKeyResult result; try { result = await fetchApiKey( - realmUrl: realmUrl.toString(), username: email, password: password); + realmUrl: realmUrl, username: email, password: password); } on Exception catch (e) { // TODO(#37): distinguish API exceptions // TODO(#35): give feedback to user on failed login debugPrint(e.toString()); @@ -139,7 +139,7 @@ class _EmailPasswordLoginPageState extends State { } final account = Account( - realmUrl: realmUrl.toString(), + realmUrl: realmUrl, email: result.email, apiKey: result.apiKey, userId: userId, diff --git a/test/api/fake_api.dart b/test/api/fake_api.dart index 63d6bd1ccf..aa779c6e86 100644 --- a/test/api/fake_api.dart +++ b/test/api/fake_api.dart @@ -3,7 +3,7 @@ import 'package:zulip/model/store.dart'; /// An [ApiConnection] that accepts and replays canned responses, for testing. class FakeApiConnection extends ApiConnection { - FakeApiConnection({required String realmUrl, required String email}) + FakeApiConnection({required Uri realmUrl, required String email}) : super(auth: Auth( realmUrl: realmUrl, email: email, apiKey: _fakeApiKey)); diff --git a/test/api/route/messages_test.dart b/test/api/route/messages_test.dart index f226d48d91..9504e2fb13 100644 --- a/test/api/route/messages_test.dart +++ b/test/api/route/messages_test.dart @@ -10,7 +10,7 @@ import 'route_checks.dart'; void main() { test('sendMessage accepts fixture realm', () async { final connection = FakeApiConnection( - realmUrl: 'https://chat.zulip.org/', email: 'self@mail.example'); + realmUrl: Uri.parse('https://chat.zulip.org/'), email: 'self@mail.example'); connection.prepare(jsonEncode(SendMessageResult(id: 42).toJson())); check(sendMessage(connection, content: 'hello', topic: 'world')) .completes(it()..id.equals(42)); @@ -18,7 +18,7 @@ void main() { test('sendMessage rejects unexpected realm', () async { final connection = FakeApiConnection( - realmUrl: 'https://chat.example/', email: 'self@mail.example'); + realmUrl: Uri.parse('https://chat.example/'), email: 'self@mail.example'); connection.prepare(jsonEncode(SendMessageResult(id: 42).toJson())); check(sendMessage(connection, content: 'hello', topic: 'world')) .throws(); diff --git a/test/example_data.dart b/test/example_data.dart index 15581891f5..6332b4c4cc 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -2,12 +2,12 @@ import 'package:zulip/api/model/initial_snapshot.dart'; import 'package:zulip/api/model/model.dart'; import 'package:zulip/model/store.dart'; -const String realmUrl = 'https://chat.example/'; +final Uri realmUrl = Uri.parse('https://chat.example/'); const String recentZulipVersion = '6.1'; const int recentZulipFeatureLevel = 164; -const Account selfAccount = Account( +final Account selfAccount = Account( realmUrl: realmUrl, email: 'self@example', apiKey: 'asdfqwer', @@ -17,7 +17,7 @@ const Account selfAccount = Account( zulipMergeBase: recentZulipVersion, ); -const Account otherAccount = Account( +final Account otherAccount = Account( realmUrl: realmUrl, email: 'other@example', apiKey: 'sdfgwert',