Skip to content

Commit cc2517e

Browse files
committed
Merge branch 'fix-user-input-string-validation-ios-682'
2 parents 2378efb + 9d4c27f commit cc2517e

12 files changed

+93
-19
lines changed

ios/MullvadSettings/AccessMethodRepository.swift

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import Combine
1010
import Foundation
1111
import MullvadLogging
12+
import MullvadTypes
1213

1314
public class AccessMethodRepository: AccessMethodRepositoryProtocol {
1415
private let logger = Logger(label: "AccessMethodRepository")
@@ -54,6 +55,9 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol {
5455
public func save(_ method: PersistentAccessMethod) {
5556
var methodStore = readApiAccessMethodStore()
5657

58+
var method = method
59+
method.name = method.name.trimmingCharacters(in: .whitespaces)
60+
5761
if let index = methodStore.accessMethods.firstIndex(where: { $0.id == method.id }) {
5862
methodStore.accessMethods[index] = method
5963
} else {

ios/MullvadSettings/CustomListRepository.swift

+19-4
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,29 @@ import Foundation
1111
import MullvadLogging
1212
import MullvadTypes
1313

14-
public enum CustomRelayListError: LocalizedError, Equatable {
14+
public enum CustomRelayListError: LocalizedError, Hashable {
1515
case duplicateName
16+
case nameTooLong
1617

1718
public var errorDescription: String? {
1819
switch self {
1920
case .duplicateName:
2021
NSLocalizedString(
2122
"DUPLICATE_CUSTOM_LISTS_ERROR",
2223
tableName: "CustomLists",
23-
value: "Name is already taken.",
24+
value: "A custom list with this name exists, please choose a unique name.",
2425
comment: ""
2526
)
27+
case .nameTooLong:
28+
String(
29+
format: NSLocalizedString(
30+
"CUSTOM_LIST_NAME_TOO_LONG_ERROR",
31+
tableName: "CustomLists",
32+
value: "Name should be no longer than %i characters.",
33+
comment: ""
34+
),
35+
NameInputFormatter.maxLength
36+
)
2637
}
2738
}
2839
}
@@ -37,11 +48,15 @@ public struct CustomListRepository: CustomListRepositoryProtocol {
3748
public init() {}
3849

3950
public func save(list: CustomList) throws {
40-
var list = list
41-
list.name = list.name.trimmingCharacters(in: .whitespaces)
51+
guard list.name.count <= NameInputFormatter.maxLength else {
52+
throw CustomRelayListError.nameTooLong
53+
}
4254

4355
var lists = fetchAll()
4456

57+
var list = list
58+
list.name = list.name.trimmingCharacters(in: .whitespaces)
59+
4560
if let listWithSameName = lists.first(where: { $0.name.compare(list.name) == .orderedSame }),
4661
listWithSameName.id != list.id {
4762
throw CustomRelayListError.duplicateName

ios/MullvadSettings/PersistentAccessMethod.swift

+13
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ public struct PersistentAccessMethod: Identifiable, Codable, Equatable {
4040
self.proxyConfiguration = proxyConfiguration
4141
}
4242

43+
public init(from decoder: any Decoder) throws {
44+
let container = try decoder.container(keyedBy: CodingKeys.self)
45+
46+
self.id = try container.decode(UUID.self, forKey: .id)
47+
self.isEnabled = try container.decode(Bool.self, forKey: .isEnabled)
48+
self.proxyConfiguration = try container.decode(PersistentProxyConfiguration.self, forKey: .proxyConfiguration)
49+
50+
// Added after release of API access methods feature. There was previously no limitation on text input length,
51+
// so this formatting has been added to prevent already stored names from being too long when displayed.
52+
let name = try container.decode(String.self, forKey: .name)
53+
self.name = NameInputFormatter.format(name)
54+
}
55+
4356
public static func == (lhs: Self, rhs: Self) -> Bool {
4457
lhs.id == rhs.id
4558
}
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// NameInputFormatter.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2024-05-13.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
public struct NameInputFormatter {
10+
public static let maxLength = 30
11+
12+
public static func format(_ string: String, maxLength: Int = Self.maxLength) -> String {
13+
String(string.trimmingCharacters(in: .whitespaces).prefix(maxLength))
14+
}
15+
}

ios/MullvadVPN.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@
540540
7A6F2FAD2AFD3DA7006D0856 /* CustomDNSViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FAC2AFD3DA7006D0856 /* CustomDNSViewController.swift */; };
541541
7A6F2FAF2AFE36E7006D0856 /* VPNSettingsInfoButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FAE2AFE36E7006D0856 /* VPNSettingsInfoButtonItem.swift */; };
542542
7A7907332BC0280A00B61F81 /* InterceptibleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */; };
543+
7A7AD14F2BF21EF200B30B3C /* NameInputFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD14D2BF21DCE00B30B3C /* NameInputFormatter.swift */; };
543544
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; };
544545
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; };
545546
7A83A0C62B29A750008B5CE7 /* APIAccessMethodsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */; };
@@ -1850,6 +1851,7 @@
18501851
7A6F2FAC2AFD3DA7006D0856 /* CustomDNSViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDNSViewController.swift; sourceTree = "<group>"; };
18511852
7A6F2FAE2AFE36E7006D0856 /* VPNSettingsInfoButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsInfoButtonItem.swift; sourceTree = "<group>"; };
18521853
7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptibleNavigationController.swift; sourceTree = "<group>"; };
1854+
7A7AD14D2BF21DCE00B30B3C /* NameInputFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameInputFormatter.swift; sourceTree = "<group>"; };
18531855
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; };
18541856
7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; };
18551857
7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAccessMethodsTests.swift; sourceTree = "<group>"; };
@@ -2544,6 +2546,7 @@
25442546
58A1AA8623F43901009F7EA6 /* Location.swift */,
25452547
5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */,
25462548
58D223D7294C8E5E0029F5F8 /* MullvadTypes.h */,
2549+
7A7AD14D2BF21DCE00B30B3C /* NameInputFormatter.swift */,
25472550
A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */,
25482551
58CAFA01298530DC00BE19F7 /* Promise.swift */,
25492552
449EBA242B975B7C00DFA4EB /* Protocols */,
@@ -5843,6 +5846,7 @@
58435846
58D22410294C90210029F5F8 /* Location.swift in Sources */,
58445847
58D22411294C90210029F5F8 /* MullvadEndpoint.swift in Sources */,
58455848
58D22412294C90210029F5F8 /* RelayConstraint.swift in Sources */,
5849+
7A7AD14F2BF21EF200B30B3C /* NameInputFormatter.swift in Sources */,
58465850
58D22413294C90210029F5F8 /* RelayConstraints.swift in Sources */,
58475851
7AF9BE8C2A321D1F00DBFEDB /* RelayFilter.swift in Sources */,
58485852
58D22414294C90210029F5F8 /* RelayLocation.swift in Sources */,

ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import Combine
10+
import MullvadTypes
1011
import UIKit
1112

1213
struct CustomListCellConfiguration {
@@ -75,7 +76,7 @@ struct CustomListCellConfiguration {
7576
contentConfiguration.setPlaceholder(type: .required)
7677
contentConfiguration.textFieldProperties = .withSmartFeaturesDisabled()
7778
contentConfiguration.inputText = subject.value.name
78-
contentConfiguration.maxLength = 30
79+
contentConfiguration.maxLength = NameInputFormatter.maxLength
7980
contentConfiguration.editingEvents.onChange = subject.bindTextAction(to: \.name)
8081

8182
cell.accessibilityIdentifier = AccessibilityIdentifier.customListEditNameFieldCell

ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift

+7-2
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,12 @@ extension CustomListDataSourceConfiguration: UITableViewDelegate {
9292
let errorsInSection = itemsWithErrors.filter { itemsInSection.contains($0) }.compactMap { item in
9393
switch item {
9494
case .name:
95-
CustomListFieldValidationError.name
95+
Array(validationErrors).filter { error in
96+
if case .name = error {
97+
return true
98+
}
99+
return false
100+
}
96101
case .addLocations, .editLocations, .deleteList:
97102
nil
98103
}
@@ -102,7 +107,7 @@ extension CustomListDataSourceConfiguration: UITableViewDelegate {
102107
case .name:
103108
let view = SettingsFieldValidationErrorContentView(
104109
configuration: SettingsFieldValidationErrorConfiguration(
105-
errors: errorsInSection.settingsFieldValidationErrors
110+
errors: errorsInSection.flatMap { $0.settingsFieldValidationErrors }
106111
)
107112
)
108113
return view

ios/MullvadVPN/Coordinators/CustomLists/CustomListValidationError.swift

+5-9
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,15 @@
77
//
88

99
import Foundation
10+
import MullvadSettings
1011

11-
enum CustomListFieldValidationError: LocalizedError {
12-
case name
12+
enum CustomListFieldValidationError: LocalizedError, Hashable {
13+
case name(CustomRelayListError)
1314

1415
var errorDescription: String? {
1516
switch self {
16-
case .name:
17-
NSLocalizedString(
18-
"CUSTOM_LISTS_VALIDATION_ERROR_EMPTY_FIELD",
19-
tableName: "CutstomLists",
20-
value: "A custom list with this name exists, please choose a unique name.",
21-
comment: ""
22-
)
17+
case let .name(error):
18+
error.errorDescription
2319
}
2420
}
2521
}

ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift

+4-2
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,10 @@ class CustomListViewController: UIViewController {
154154
try interactor.save(viewModel: subject.value)
155155
delegate?.customListDidSave(subject.value.customList)
156156
} catch {
157-
validationErrors.insert(.name)
158-
dataSourceConfiguration?.set(validationErrors: validationErrors)
157+
if let error = error as? CustomRelayListError {
158+
validationErrors.insert(.name(error))
159+
dataSourceConfiguration?.set(validationErrors: validationErrors)
160+
}
159161
}
160162
}
161163

ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsCellConfiguration.swift

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import Combine
10+
import MullvadTypes
1011
import UIKit
1112

1213
class MethodSettingsCellConfiguration {
@@ -109,6 +110,7 @@ class MethodSettingsCellConfiguration {
109110
contentConfiguration.setPlaceholder(type: .required)
110111
contentConfiguration.textFieldProperties = .withSmartFeaturesDisabled()
111112
contentConfiguration.inputText = subject.value.name
113+
contentConfiguration.maxLength = NameInputFormatter.maxLength
112114
contentConfiguration.editingEvents.onChange = subject.bindTextAction(to: \.name)
113115

114116
cell.accessibilityIdentifier = .accessMethodNameTextField

ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodValidationError.swift

+14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import Foundation
10+
import MullvadTypes
1011

1112
/// Access method validation error that holds an array of individual per-field validation errors.
1213
struct AccessMethodValidationError: LocalizedError, Equatable {
@@ -57,6 +58,9 @@ struct AccessMethodFieldValidationError: LocalizedError, Equatable {
5758

5859
/// Invalid port number, i.e zero.
5960
case invalidPort
61+
62+
/// The name input is too long.
63+
case nameTooLong
6064
}
6165

6266
/// Kind of validation error.
@@ -91,6 +95,16 @@ struct AccessMethodFieldValidationError: LocalizedError, Equatable {
9195
value: "Please enter a valid port.",
9296
comment: ""
9397
)
98+
case .nameTooLong:
99+
String(
100+
format: NSLocalizedString(
101+
"VALIDATION_ERRORS_NAME_TOO_LONG",
102+
tableName: "APIAccess",
103+
value: "Name should be no longer than %i characters.",
104+
comment: ""
105+
),
106+
NameInputFormatter.maxLength
107+
)
94108
}
95109
}
96110
}

ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel+Persistent.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,13 @@ extension AccessMethodViewModel {
6666
}
6767

6868
private func validateName() throws -> String {
69+
// Context doesn't matter for name field errors.
6970
if name.isEmpty {
70-
// Context doesn't matter for name field.
7171
let fieldError = AccessMethodFieldValidationError(kind: .emptyValue, field: .name, context: .shadowsocks)
7272
throw AccessMethodValidationError(fieldErrors: [fieldError])
73+
} else if name.count > NameInputFormatter.maxLength {
74+
let fieldError = AccessMethodFieldValidationError(kind: .nameTooLong, field: .name, context: .shadowsocks)
75+
throw AccessMethodValidationError(fieldErrors: [fieldError])
7376
}
7477

7578
return name

0 commit comments

Comments
 (0)