Skip to content

Commit 2468db2

Browse files
committed
api: Don't allow connecting to servers <4.0
From the README: https://github.com/zulip/zulip-flutter?tab=readme-ov-file#server-compatibility > We support Zulip Server 4.0 and later. This implementation isn't ideal because it logs you out, instead of just taking down the account's UI as we discussed: https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/.23F267.20Disallow.20connecting.20to.20unsupported.20ancient.20servers/near/2104182 But it was simpler this way since programmatically logging out is already an action we handle. Fixes: #267
1 parent ad292ca commit 2468db2

16 files changed

+268
-6
lines changed

Diff for: assets/l10n/app_en.arb

+9
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,15 @@
538538
"@topicValidationErrorMandatoryButEmpty": {
539539
"description": "Topic validation error when topic is required but was empty."
540540
},
541+
"errorServerVersionUnsupportedMessage": "{url} is running Zulip Server {zulipVersion}, which is unsupported. The minimum supported version is Zulip Server {minSupportedZulipVersion}.",
542+
"@errorServerVersionUnsupportedMessage": {
543+
"description": "Error message in the dialog for when the Zulip Server version is unsupported.",
544+
"placeholders": {
545+
"url": {"type": "String", "example": "http://chat.example.com/"},
546+
"zulipVersion": {"type": "String", "example": "3.2"},
547+
"minSupportedZulipVersion": {"type": "String", "example": "4.0"}
548+
}
549+
},
541550
"errorInvalidApiKeyMessage": "Your account at {url} could not be authenticated. Please try logging in again or use another account.",
542551
"@errorInvalidApiKeyMessage": {
543552
"description": "Error message in the dialog for invalid API key.",

Diff for: lib/api/core.dart

+18
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ import '../model/binding.dart';
1010
import '../model/localizations.dart';
1111
import 'exception.dart';
1212

13+
/// The Zulip Server version below which we should refuse to connect.
14+
///
15+
/// When updating this, also update [kMinSupportedZulipFeatureLevel]
16+
/// and the README.
17+
const kMinSupportedZulipVersion = '4.0';
18+
19+
/// The Zulip feature level reserved for the [kMinSupportedZulipVersion] release.
20+
///
21+
/// For this value, see the API changelog:
22+
/// https://zulip.com/api/changelog
23+
const kMinSupportedZulipFeatureLevel = 65;
24+
25+
/// The doc stating our oldest supported server version.
26+
// TODO: Instead, link to new Help Center doc once we have it:
27+
// https://github.com/zulip/zulip/issues/23842
28+
final kServerSupportDocUrl = Uri.parse(
29+
'https://zulip.readthedocs.io/en/latest/overview/release-lifecycle.html#compatibility-and-upgrading');
30+
1331
/// A fused JSON + UTF-8 decoder.
1432
///
1533
/// This object is an instance of [`_JsonUtf8Decoder`][1] which is

Diff for: lib/generated/l10n/zulip_localizations.dart

+6
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,12 @@ abstract class ZulipLocalizations {
825825
/// **'Topics are required in this organization.'**
826826
String get topicValidationErrorMandatoryButEmpty;
827827

828+
/// Error message in the dialog for when the Zulip Server version is unsupported.
829+
///
830+
/// In en, this message translates to:
831+
/// **'{url} is running Zulip Server {zulipVersion}, which is unsupported. The minimum supported version is Zulip Server {minSupportedZulipVersion}.'**
832+
String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion);
833+
828834
/// Error message in the dialog for invalid API key.
829835
///
830836
/// In en, this message translates to:

Diff for: lib/generated/l10n/zulip_localizations_ar.dart

+5
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,11 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
416416
@override
417417
String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.';
418418

419+
@override
420+
String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) {
421+
return '$url is running Zulip Server $zulipVersion, which is unsupported. The minimum supported version is Zulip Server $minSupportedZulipVersion.';
422+
}
423+
419424
@override
420425
String errorInvalidApiKeyMessage(String url) {
421426
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

Diff for: lib/generated/l10n/zulip_localizations_en.dart

+5
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,11 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
416416
@override
417417
String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.';
418418

419+
@override
420+
String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) {
421+
return '$url is running Zulip Server $zulipVersion, which is unsupported. The minimum supported version is Zulip Server $minSupportedZulipVersion.';
422+
}
423+
419424
@override
420425
String errorInvalidApiKeyMessage(String url) {
421426
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

Diff for: lib/generated/l10n/zulip_localizations_ja.dart

+5
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,11 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
416416
@override
417417
String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.';
418418

419+
@override
420+
String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) {
421+
return '$url is running Zulip Server $zulipVersion, which is unsupported. The minimum supported version is Zulip Server $minSupportedZulipVersion.';
422+
}
423+
419424
@override
420425
String errorInvalidApiKeyMessage(String url) {
421426
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

Diff for: lib/generated/l10n/zulip_localizations_nb.dart

+5
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,11 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
416416
@override
417417
String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.';
418418

419+
@override
420+
String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) {
421+
return '$url is running Zulip Server $zulipVersion, which is unsupported. The minimum supported version is Zulip Server $minSupportedZulipVersion.';
422+
}
423+
419424
@override
420425
String errorInvalidApiKeyMessage(String url) {
421426
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

Diff for: lib/generated/l10n/zulip_localizations_pl.dart

+5
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,11 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
416416
@override
417417
String get topicValidationErrorMandatoryButEmpty => 'Wątki są wymagane przez tę organizację.';
418418

419+
@override
420+
String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) {
421+
return '$url is running Zulip Server $zulipVersion, which is unsupported. The minimum supported version is Zulip Server $minSupportedZulipVersion.';
422+
}
423+
419424
@override
420425
String errorInvalidApiKeyMessage(String url) {
421426
return 'Konto w ramach $url nie zostało przyjęte. Spróbuj ponownie lub skorzystaj z innego konta.';

Diff for: lib/generated/l10n/zulip_localizations_ru.dart

+5
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,11 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
416416
@override
417417
String get topicValidationErrorMandatoryButEmpty => 'Темы обязательны в этой организации.';
418418

419+
@override
420+
String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) {
421+
return '$url is running Zulip Server $zulipVersion, which is unsupported. The minimum supported version is Zulip Server $minSupportedZulipVersion.';
422+
}
423+
419424
@override
420425
String errorInvalidApiKeyMessage(String url) {
421426
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

Diff for: lib/generated/l10n/zulip_localizations_sk.dart

+5
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,11 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
416416
@override
417417
String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.';
418418

419+
@override
420+
String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) {
421+
return '$url is running Zulip Server $zulipVersion, which is unsupported. The minimum supported version is Zulip Server $minSupportedZulipVersion.';
422+
}
423+
419424
@override
420425
String errorInvalidApiKeyMessage(String url) {
421426
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

Diff for: lib/log.dart

+10-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ bool debugLog(String message) {
3636
// `null` for the `message` parameter and promptly dismiss the reported errors.
3737
typedef ReportErrorCancellablyCallback = void Function(String? message, {String? details});
3838

39-
typedef ReportErrorCallback = void Function(String title, {String? message});
39+
typedef ReportErrorCallback = void Function(
40+
String title, {
41+
String? message,
42+
Uri? learnMoreButtonUrl,
43+
});
4044

4145
/// Show the user an error message, without requiring them to interact with it.
4246
///
@@ -70,7 +74,11 @@ void defaultReportErrorToUserBriefly(String? message, {String? details}) {
7074
_reportErrorToConsole(message, details);
7175
}
7276

73-
void defaultReportErrorToUserModally(String title, {String? message}) {
77+
void defaultReportErrorToUserModally(
78+
String title, {
79+
String? message,
80+
Uri? learnMoreButtonUrl,
81+
}) {
7482
_reportErrorToConsole(title, message);
7583
}
7684

Diff for: lib/model/store.dart

+54-2
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,20 @@ abstract class GlobalStore extends ChangeNotifier {
199199
assert(account != null); // doLoadPerAccount would have thrown AccountNotFoundException
200200
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
201201
switch (e) {
202+
case _ServerVersionUnsupportedException():
203+
reportErrorToUserModally(
204+
zulipLocalizations.errorCouldNotConnectTitle,
205+
message: zulipLocalizations.errorServerVersionUnsupportedMessage(
206+
account!.realmUrl.toString(),
207+
e.data.zulipVersion,
208+
kMinSupportedZulipVersion),
209+
learnMoreButtonUrl: kServerSupportDocUrl);
210+
// The important thing is to tear down per-account UI,
211+
// and logOutAccount conveniently handles that already.
212+
// It's not ideal to force the user to reauthenticate when they retry,
213+
// and we can revisit that later if needed.
214+
await logOutAccount(this, accountId);
215+
throw AccountNotFoundException();
202216
case HttpException(httpStatus: 401):
203217
// The API key is invalid and the store can never be loaded
204218
// unless the user retries manually.
@@ -1009,8 +1023,15 @@ class UpdateMachine {
10091023
}
10101024

10111025
final stopwatch = Stopwatch()..start();
1012-
final initialSnapshot = await _registerQueueWithRetry(connection,
1013-
stopAndThrowIfNoAccount: stopAndThrowIfNoAccount);
1026+
InitialSnapshot? initialSnapshot;
1027+
try {
1028+
initialSnapshot = await _registerQueueWithRetry(connection,
1029+
stopAndThrowIfNoAccount: stopAndThrowIfNoAccount);
1030+
} on _ServerVersionUnsupportedException catch (e) {
1031+
connection.close();
1032+
await updateZulipVersionData(e.data);
1033+
rethrow;
1034+
}
10141035
final t = (stopwatch..stop()).elapsed;
10151036
assert(debugLog("initial fetch time: ${t.inMilliseconds}ms"));
10161037

@@ -1059,7 +1080,12 @@ class UpdateMachine {
10591080
} catch (e, s) {
10601081
stopAndThrowIfNoAccount();
10611082
// TODO(#890): tell user if initial-fetch errors persist, or look non-transient
1083+
final ZulipVersionData? zulipVersionData;
10621084
switch (e) {
1085+
case MalformedServerResponseException()
1086+
when (zulipVersionData = ZulipVersionData.fromMalformedServerResponseException(e))
1087+
?.isUnsupported == true:
1088+
throw _ServerVersionUnsupportedException(zulipVersionData!);
10631089
case HttpException(httpStatus: 401):
10641090
// We cannot recover from this error through retrying.
10651091
// Leave it to [GlobalStore.loadPerAccount].
@@ -1077,6 +1103,10 @@ class UpdateMachine {
10771103
}
10781104
if (result != null) {
10791105
stopAndThrowIfNoAccount();
1106+
final zulipVersionData = ZulipVersionData.fromInitialSnapshot(result);
1107+
if (zulipVersionData.isUnsupported) {
1108+
throw _ServerVersionUnsupportedException(zulipVersionData);
1109+
}
10801110
return result;
10811111
}
10821112
}
@@ -1499,9 +1529,31 @@ class ZulipVersionData {
14991529
zulipMergeBase: initialSnapshot.zulipMergeBase,
15001530
zulipFeatureLevel: initialSnapshot.zulipFeatureLevel);
15011531

1532+
/// Make a [ZulipVersionData] from a [MalformedServerResponseException],
1533+
/// if the body was readable/valid JSON and contained the data, else null.
1534+
static ZulipVersionData? fromMalformedServerResponseException(MalformedServerResponseException e) {
1535+
try {
1536+
final data = e.data!;
1537+
return ZulipVersionData(
1538+
zulipVersion: data['zulip_version'] as String,
1539+
zulipMergeBase: data['zulip_merge_base'] as String?,
1540+
zulipFeatureLevel: data['zulip_feature_level'] as int);
1541+
} catch (inner) {
1542+
return null;
1543+
}
1544+
}
1545+
15021546
final String zulipVersion;
15031547
final String? zulipMergeBase;
15041548
final int zulipFeatureLevel;
1549+
1550+
bool get isUnsupported => zulipFeatureLevel < kMinSupportedZulipFeatureLevel;
1551+
}
1552+
1553+
class _ServerVersionUnsupportedException implements Exception {
1554+
final ZulipVersionData data;
1555+
1556+
_ServerVersionUnsupportedException(this.data);
15051557
}
15061558

15071559
class _EventHandlingException implements Exception {

Diff for: lib/widgets/app.dart

+7-2
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,18 @@ class ZulipApp extends StatefulWidget {
130130
}
131131

132132
/// The callback we normally use as [reportErrorToUserModally].
133-
static void _reportErrorToUserModally(String title, {String? message}) {
133+
static void _reportErrorToUserModally(
134+
String title, {
135+
String? message,
136+
Uri? learnMoreButtonUrl,
137+
}) {
134138
assert(_ready.value);
135139

136140
showErrorDialog(
137141
context: navigatorKey.currentContext!,
138142
title: title,
139-
message: message);
143+
message: message,
144+
learnMoreButtonUrl: learnMoreButtonUrl);
140145
}
141146

142147
void _declareReady() {

Diff for: test/example_data.dart

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:convert';
22
import 'dart:math';
33

4+
import 'package:zulip/api/core.dart';
45
import 'package:zulip/api/exception.dart';
56
import 'package:zulip/api/model/events.dart';
67
import 'package:zulip/api/model/initial_snapshot.dart';
@@ -76,6 +77,7 @@ Uri get _realmUrl => realmUrl;
7677
const String recentZulipVersion = '9.0';
7778
const int recentZulipFeatureLevel = 278;
7879
const int futureZulipFeatureLevel = 9999;
80+
const int ancientZulipFeatureLevel = kMinSupportedZulipFeatureLevel - 1;
7981

8082
GetServerSettingsResult serverSettings({
8183
Map<String, bool>? authenticationMethods,

0 commit comments

Comments
 (0)