diff --git a/i18n/en.json b/i18n/en.json index a435c5986e..32b0474147 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -782,6 +782,8 @@ "client_cert_import": "Import", "client_cert_import_success_msg": "Client certificate is imported", "client_cert_invalid_msg": "Invalid certificate file or wrong password", + "client_cert_password_message": "Enter the password for this certificate", + "client_cert_password_title": "Certificate Password", "client_cert_remove_msg": "Client certificate is removed", "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate import/removal is available only before login", "client_cert_title": "SSL client certificate [EXPERIMENTAL]", diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt index aac9b6c806..1e7156a147 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt @@ -108,6 +108,43 @@ data class ClientCertData ( override fun hashCode(): Int = toList().hashCode() } + +/** Generated class from Pigeon that represents data sent in messages. */ +data class ClientCertPrompt ( + val title: String, + val message: String, + val cancel: String, + val confirm: String +) + { + companion object { + fun fromList(pigeonVar_list: List): ClientCertPrompt { + val title = pigeonVar_list[0] as String + val message = pigeonVar_list[1] as String + val cancel = pigeonVar_list[2] as String + val confirm = pigeonVar_list[3] as String + return ClientCertPrompt(title, message, cancel, confirm) + } + } + fun toList(): List { + return listOf( + title, + message, + cancel, + confirm, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is ClientCertPrompt) { + return false + } + if (this === other) { + return true + } + return NetworkPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} private open class NetworkPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { @@ -116,6 +153,11 @@ private open class NetworkPigeonCodec : StandardMessageCodec() { ClientCertData.fromList(it) } } + 130.toByte() -> { + return (readValue(buffer) as? List)?.let { + ClientCertPrompt.fromList(it) + } + } else -> super.readValueOfType(type, buffer) } } @@ -125,6 +167,10 @@ private open class NetworkPigeonCodec : StandardMessageCodec() { stream.write(129) writeValue(stream, value.toList()) } + is ClientCertPrompt -> { + stream.write(130) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -134,7 +180,7 @@ private open class NetworkPigeonCodec : StandardMessageCodec() { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface NetworkApi { fun addCertificate(clientData: ClientCertData, callback: (Result) -> Unit) - fun selectCertificate(callback: (Result) -> Unit) + fun selectCertificate(promptText: ClientCertPrompt, callback: (Result) -> Unit) fun removeCertificate(callback: (Result) -> Unit) companion object { @@ -168,8 +214,10 @@ interface NetworkApi { run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$separatedMessageChannelSuffix", codec) if (api != null) { - channel.setMessageHandler { _, reply -> - api.selectCertificate{ result: Result -> + channel.setMessageHandler { message, reply -> + val args = message as List + val promptTextArg = args[0] as ClientCertPrompt + api.selectCertificate(promptTextArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { reply.reply(NetworkPigeonUtils.wrapError(error)) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt index 6c3decfa5c..4ef8f54b13 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt @@ -54,6 +54,7 @@ private class NetworkApiImpl(private val context: Context) : NetworkApi { private var activity: Activity? = null private var pendingCallback: ((Result) -> Unit)? = null private var filePicker: ActivityResultLauncher>? = null + private var promptText: ClientCertPrompt? = null fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity @@ -85,9 +86,10 @@ private class NetworkApiImpl(private val context: Context) : NetworkApi { } } - override fun selectCertificate(callback: (Result) -> Unit) { + override fun selectCertificate(promptText: ClientCertPrompt, callback: (Result) -> Unit) { val picker = filePicker ?: return callback(Result.failure(IllegalStateException("No activity"))) pendingCallback = callback + this.promptText = promptText picker.launch(arrayOf("application/x-pkcs12", "application/x-pem-file")) } @@ -106,6 +108,7 @@ private class NetworkApiImpl(private val context: Context) : NetworkApi { val activity = activity ?: throw IllegalStateException("No activity") promptForPassword(activity) { password -> + promptText = null if (password == null) { callback(Result.failure(OperationCanceledException())) return@promptForPassword @@ -143,12 +146,13 @@ private class NetworkApiImpl(private val context: Context) : NetworkApi { val container = FrameLayout(themedContext).apply { addView(textInputLayout) } + val text = promptText!! MaterialAlertDialogBuilder(themedContext) - .setTitle("Certificate Password") - .setMessage("Enter the password for this certificate") + .setTitle(text.title) + .setMessage(text.message) .setView(container) - .setPositiveButton("Import") { _, _ -> callback(editText.text.toString()) } - .setNegativeButton("Cancel") { dialog, _ -> + .setPositiveButton(text.confirm) { _, _ -> callback(editText.text.toString()) } + .setNegativeButton(text.cancel) { dialog, _ -> dialog.cancel() callback(null) } diff --git a/mobile/ios/Runner/Core/Network.g.swift b/mobile/ios/Runner/Core/Network.g.swift index 7e47ece5cb..0f678ce4a4 100644 --- a/mobile/ios/Runner/Core/Network.g.swift +++ b/mobile/ios/Runner/Core/Network.g.swift @@ -139,11 +139,50 @@ struct ClientCertData: Hashable { } } +/// Generated class from Pigeon that represents data sent in messages. +struct ClientCertPrompt: Hashable { + var title: String + var message: String + var cancel: String + var confirm: String + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> ClientCertPrompt? { + let title = pigeonVar_list[0] as! String + let message = pigeonVar_list[1] as! String + let cancel = pigeonVar_list[2] as! String + let confirm = pigeonVar_list[3] as! String + + return ClientCertPrompt( + title: title, + message: message, + cancel: cancel, + confirm: confirm + ) + } + func toList() -> [Any?] { + return [ + title, + message, + cancel, + confirm, + ] + } + static func == (lhs: ClientCertPrompt, rhs: ClientCertPrompt) -> Bool { + return deepEqualsNetwork(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashNetwork(value: toList(), hasher: &hasher) + } +} + private class NetworkPigeonCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { case 129: return ClientCertData.fromList(self.readValue() as! [Any?]) + case 130: + return ClientCertPrompt.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) } @@ -155,6 +194,9 @@ private class NetworkPigeonCodecWriter: FlutterStandardWriter { if let value = value as? ClientCertData { super.writeByte(129) super.writeValue(value.toList()) + } else if let value = value as? ClientCertPrompt { + super.writeByte(130) + super.writeValue(value.toList()) } else { super.writeValue(value) } @@ -179,7 +221,7 @@ class NetworkPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol NetworkApi { func addCertificate(clientData: ClientCertData, completion: @escaping (Result) -> Void) - func selectCertificate(completion: @escaping (Result) -> Void) + func selectCertificate(promptText: ClientCertPrompt, completion: @escaping (Result) -> Void) func removeCertificate(completion: @escaping (Result) -> Void) } @@ -208,8 +250,10 @@ class NetworkApiSetup { } let selectCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { - selectCertificateChannel.setMessageHandler { _, reply in - api.selectCertificate { result in + selectCertificateChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let promptTextArg = args[0] as! ClientCertPrompt + api.selectCertificate(promptText: promptTextArg) { result in switch result { case .success(let res): reply(wrapResult(res)) diff --git a/mobile/ios/Runner/Core/NetworkApiImpl.swift b/mobile/ios/Runner/Core/NetworkApiImpl.swift index 07fef68bf1..d67c392a3a 100644 --- a/mobile/ios/Runner/Core/NetworkApiImpl.swift +++ b/mobile/ios/Runner/Core/NetworkApiImpl.swift @@ -16,8 +16,8 @@ class NetworkApiImpl: NetworkApi { self.viewController = viewController } - func selectCertificate(completion: @escaping (Result) -> Void) { - let importer = CertImporter(completion: { [weak self] result in + func selectCertificate(promptText: ClientCertPrompt, completion: @escaping (Result) -> Void) { + let importer = CertImporter(promptText: promptText, completion: { [weak self] result in self?.activeImporter = nil completion(result.map { ClientCertData(data: FlutterStandardTypedData(bytes: $0.0), password: $0.1) }) }, viewController: viewController) @@ -43,10 +43,12 @@ class NetworkApiImpl: NetworkApi { } private class CertImporter: NSObject, UIDocumentPickerDelegate { + private let promptText: ClientCertPrompt private var completion: ((Result<(Data, String), Error>) -> Void) private weak var viewController: UIViewController? - init(completion: (@escaping (Result<(Data, String), Error>) -> Void), viewController: UIViewController?) { + init(promptText: ClientCertPrompt, completion: (@escaping (Result<(Data, String), Error>) -> Void), viewController: UIViewController?) { + self.promptText = promptText self.completion = completion self.viewController = viewController } @@ -95,18 +97,18 @@ private class CertImporter: NSObject, UIDocumentPickerDelegate { return await withCheckedContinuation { continuation in let alert = UIAlertController( - title: "Certificate Password", - message: "Enter the password for this certificate", + title: promptText.title, + message: promptText.message, preferredStyle: .alert ) alert.addTextField { $0.isSecureTextEntry = true } - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in + alert.addAction(UIAlertAction(title: promptText.cancel, style: .cancel) { _ in continuation.resume(returning: nil) }) - alert.addAction(UIAlertAction(title: "Import", style: .default) { _ in + alert.addAction(UIAlertAction(title: promptText.confirm, style: .default) { _ in continuation.resume(returning: alert.textFields?.first?.text ?? "") }) diff --git a/mobile/lib/platform/network_api.g.dart b/mobile/lib/platform/network_api.g.dart index 8af9e02086..6ddb3cdb71 100644 --- a/mobile/lib/platform/network_api.g.dart +++ b/mobile/lib/platform/network_api.g.dart @@ -66,6 +66,52 @@ class ClientCertData { int get hashCode => Object.hashAll(_toList()); } +class ClientCertPrompt { + ClientCertPrompt({required this.title, required this.message, required this.cancel, required this.confirm}); + + String title; + + String message; + + String cancel; + + String confirm; + + List _toList() { + return [title, message, cancel, confirm]; + } + + Object encode() { + return _toList(); + } + + static ClientCertPrompt decode(Object result) { + result as List; + return ClientCertPrompt( + title: result[0]! as String, + message: result[1]! as String, + cancel: result[2]! as String, + confirm: result[3]! as String, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! ClientCertPrompt || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -76,6 +122,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is ClientCertData) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is ClientCertPrompt) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -86,6 +135,8 @@ class _PigeonCodec extends StandardMessageCodec { switch (type) { case 129: return ClientCertData.decode(readValue(buffer)!); + case 130: + return ClientCertPrompt.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -128,7 +179,7 @@ class NetworkApi { } } - Future selectCertificate() async { + Future selectCertificate(ClientCertPrompt promptText) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( @@ -136,7 +187,7 @@ class NetworkApi { pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([promptText]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); diff --git a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart index 75ba7fec56..4e224a0567 100644 --- a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart +++ b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/platform/network_api.g.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:logging/logging.dart'; @@ -67,7 +68,13 @@ class _SslClientCertSettingsState extends State { Future importCert() async { try { - final cert = await networkApi.selectCertificate(); + final styling = ClientCertPrompt( + title: "client_cert_password_title".tr(), + message: "client_cert_password_message".tr(), + cancel: "cancel".tr(), + confirm: "confirm".tr(), + ); + final cert = await networkApi.selectCertificate(styling); await SSLClientCertStoreVal(cert.data, cert.password).save(); HttpSSLOptions.apply(); setState(() => isCertExist = true); diff --git a/mobile/pigeon/network_api.dart b/mobile/pigeon/network_api.dart index 17b04a2369..68d2f7d8fc 100644 --- a/mobile/pigeon/network_api.dart +++ b/mobile/pigeon/network_api.dart @@ -7,6 +7,15 @@ class ClientCertData { ClientCertData(this.data, this.password); } +class ClientCertPrompt { + String title; + String message; + String cancel; + String confirm; + + ClientCertPrompt(this.title, this.message, this.cancel, this.confirm); +} + @ConfigurePigeon( PigeonOptions( dartOut: 'lib/platform/network_api.g.dart', @@ -25,7 +34,7 @@ abstract class NetworkApi { void addCertificate(ClientCertData clientData); @async - ClientCertData selectCertificate(); + ClientCertData selectCertificate(ClientCertPrompt promptText); @async void removeCertificate();