Skip to content

Commit b38efb2

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 40b1426 commit b38efb2

15 files changed

+264
-6
lines changed

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.",

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 [minSupportedZulipVersion] 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

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:

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.';

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.';

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.';

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.';

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 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

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.';

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.';

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

lib/model/store.dart

+54-2
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,20 @@ abstract class GlobalStore extends ChangeNotifier {
193193
assert(account != null); // doLoadPerAccount would have thrown AccountNotFoundException
194194
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
195195
switch (e) {
196+
case _ServerVersionUnsupportedException():
197+
reportErrorToUserModally(
198+
zulipLocalizations.errorCouldNotConnectTitle,
199+
message: zulipLocalizations.errorServerVersionUnsupportedMessage(
200+
account!.realmUrl.toString(),
201+
e.data.zulipVersion,
202+
kMinSupportedZulipVersion),
203+
learnMoreButtonUrl: kServerSupportDocUrl);
204+
// The important thing is to tear down per-account UI,
205+
// and logOutAccount conveniently handles that already.
206+
// It's not ideal to force the user to reauthenticate when they retry,
207+
// and we can revisit that later if needed.
208+
await logOutAccount(this, accountId);
209+
throw AccountNotFoundException();
196210
case HttpException(httpStatus: 401):
197211
// The API key is invalid and the store can never be loaded
198212
// unless the user retries manually.
@@ -984,8 +998,15 @@ class UpdateMachine {
984998
}
985999

9861000
final stopwatch = Stopwatch()..start();
987-
final initialSnapshot = await _registerQueueWithRetry(connection,
988-
stopAndThrowIfNoAccount: stopAndThrowIfNoAccount);
1001+
InitialSnapshot? initialSnapshot;
1002+
try {
1003+
initialSnapshot = await _registerQueueWithRetry(connection,
1004+
stopAndThrowIfNoAccount: stopAndThrowIfNoAccount);
1005+
} on _ServerVersionUnsupportedException catch (e) {
1006+
connection.close();
1007+
await updateZulipVersionData(e.data);
1008+
rethrow;
1009+
}
9891010
final t = (stopwatch..stop()).elapsed;
9901011
assert(debugLog("initial fetch time: ${t.inMilliseconds}ms"));
9911012

@@ -1034,7 +1055,12 @@ class UpdateMachine {
10341055
} catch (e, s) {
10351056
stopAndThrowIfNoAccount();
10361057
// TODO(#890): tell user if initial-fetch errors persist, or look non-transient
1058+
final ZulipVersionData? zulipVersionData;
10371059
switch (e) {
1060+
case MalformedServerResponseException()
1061+
when (zulipVersionData = ZulipVersionData.fromMalformedServerResponseException(e))
1062+
?.isUnsupported == true:
1063+
throw _ServerVersionUnsupportedException(zulipVersionData!);
10381064
case HttpException(httpStatus: 401):
10391065
// We cannot recover from this error through retrying.
10401066
// Leave it to [GlobalStore.loadPerAccount].
@@ -1052,6 +1078,10 @@ class UpdateMachine {
10521078
}
10531079
if (result != null) {
10541080
stopAndThrowIfNoAccount();
1081+
final zulipVersionData = ZulipVersionData.fromInitialSnapshot(result);
1082+
if (zulipVersionData.isUnsupported) {
1083+
throw _ServerVersionUnsupportedException(zulipVersionData);
1084+
}
10551085
return result;
10561086
}
10571087
}
@@ -1474,9 +1504,31 @@ class ZulipVersionData {
14741504
zulipMergeBase: initialSnapshot.zulipMergeBase,
14751505
zulipFeatureLevel: initialSnapshot.zulipFeatureLevel);
14761506

1507+
/// A [ZulipVersionData] from a [MalformedServerResponseException],
1508+
/// if the body was readable/valid JSON and contained the data, else null.
1509+
static ZulipVersionData? fromMalformedServerResponseException(MalformedServerResponseException e) {
1510+
try {
1511+
final data = e.data!;
1512+
return ZulipVersionData(
1513+
zulipVersion: data['zulip_version'] as String,
1514+
zulipMergeBase: data['zulip_merge_base'] as String?,
1515+
zulipFeatureLevel: data['zulip_feature_level'] as int);
1516+
} catch (inner) {
1517+
return null;
1518+
}
1519+
}
1520+
14771521
final String zulipVersion;
14781522
final String? zulipMergeBase;
14791523
final int zulipFeatureLevel;
1524+
1525+
bool get isUnsupported => zulipFeatureLevel < kMinSupportedZulipFeatureLevel;
1526+
}
1527+
1528+
class _ServerVersionUnsupportedException implements Exception {
1529+
final ZulipVersionData data;
1530+
1531+
_ServerVersionUnsupportedException(this.data);
14801532
}
14811533

14821534
class _EventHandlingException implements Exception {

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() {

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';
@@ -77,6 +78,7 @@ Uri get _realmUrl => realmUrl;
7778
const String recentZulipVersion = '9.0';
7879
const int recentZulipFeatureLevel = 278;
7980
const int futureZulipFeatureLevel = 9999;
81+
const int ancientZulipFeatureLevel = kMinSupportedZulipFeatureLevel - 1;
8082

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

0 commit comments

Comments
 (0)