diff --git a/RIADigiDoc.xcodeproj/project.pbxproj b/RIADigiDoc.xcodeproj/project.pbxproj index d4a91dd2..4f31250d 100644 --- a/RIADigiDoc.xcodeproj/project.pbxproj +++ b/RIADigiDoc.xcodeproj/project.pbxproj @@ -263,6 +263,7 @@ Util/Signature/SignatureUtilProtocol.swift, Util/Theme/ThemeSettings.swift, Util/Theme/ThemeSettingsProtocol.swift, + Util/WebEid/WebEidUriUtil.swift, ViewModel/AdvancedSettingsViewModel.swift, ViewModel/CertificateDetailViewModel.swift, ViewModel/Crypto/DecryptRootViewModel.swift, diff --git a/RIADigiDoc/RIADigiDoc.entitlements b/RIADigiDoc/RIADigiDoc.entitlements index cc9ae069..0ddb2583 100644 --- a/RIADigiDoc/RIADigiDoc.entitlements +++ b/RIADigiDoc/RIADigiDoc.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.associated-domains + + applinks:riadigidoc.ee + com.apple.developer.nfc.readersession.formats TAG diff --git a/RIADigiDoc/UI/Component/HomeView.swift b/RIADigiDoc/UI/Component/HomeView.swift index b586261a..9f2bec87 100644 --- a/RIADigiDoc/UI/Component/HomeView.swift +++ b/RIADigiDoc/UI/Component/HomeView.swift @@ -308,7 +308,7 @@ struct HomeView: View { } private func handleIncoming(url: URL) { - let webEidURL = (url.scheme == "web-eid-mobile") ? url : nil + let webEidURL = (WebEidUriUtil.isWebEidUri(url)) ? url : nil let externalFileURLs: [URL] = (webEidURL != nil) ? [] : getExternalFileURLs(from: url) handleFiles(externalFileURLs) diff --git a/RIADigiDoc/UI/Component/WebEid/WebEidView.swift b/RIADigiDoc/UI/Component/WebEid/WebEidView.swift index 807f6284..0d488917 100644 --- a/RIADigiDoc/UI/Component/WebEid/WebEidView.swift +++ b/RIADigiDoc/UI/Component/WebEid/WebEidView.swift @@ -50,6 +50,52 @@ struct WebEidView: View { ) } + private var alertTitle: String { + languageSettings.localized( + viewModel.alertMessageKey ?? "", + viewModel.alertMessageExtraArguments + ) + } + + private var alertInfoURL: URL? { + guard + let messageUrl = viewModel.alertMessageUrl, + !messageUrl.isEmpty + else { + return nil + } + + let localizedUrl = languageSettings.localized(messageUrl) + guard let url = URL(string: localizedUrl), UIApplication.shared.canOpenURL(url) else { + return nil + } + + return url + } + + private func handleWebEidOperation(for url: URL) { + let operation = WebEidUriUtil.getOperation(from: url) + switch operation { + case .auth: + viewModel.handleAuth(url: url) + case .cert: + viewModel.handleCertificate(url: url) + case .sign: + viewModel.handleSign(url: url) + case .unknown: + viewModel.handleUnknown(url: url) + } + } + + private func activateWebEidSession() { + Task { + if await viewModel.isWebEidSessionActive() { + await nfcViewModel.clearTempCAN() + } + await viewModel.setWebEidSessionActive(true) + } + } + init( webEidUrl: URL, ) { @@ -119,40 +165,22 @@ struct WebEidView: View { } } .alert( - languageSettings.localized( - viewModel.alertMessageKey ?? "", - viewModel.alertMessageExtraArguments - ), + alertTitle, isPresented: $viewModel.showAlertMessage ) { Button(languageSettings.localized("OK")) { viewModel.resetErrors() } - if let messageUrl = viewModel.alertMessageUrl, !messageUrl.isEmpty { + if let alertInfoURL { Button(languageSettings.localized("Additional information")) { - if let url = URL(string: languageSettings.localized(messageUrl)), - UIApplication.shared.canOpenURL(url) { - openURL(url) - } + openURL(alertInfoURL) viewModel.resetErrors() } } } .onAppear { - if let host = webEidUrl.host { - - switch host { - case "auth": - viewModel.handleAuth(url: webEidUrl) - case "cert": - viewModel.handleCertificate(url: webEidUrl) - case "sign": - viewModel.handleSign(url: webEidUrl) - default: - viewModel.handleUnknown(url: webEidUrl) - } - } + handleWebEidOperation(for: webEidUrl) } .onChange(of: viewModel.relyingPartyResponseEvents) { _, responseURL in guard let responseURL else { return } @@ -166,35 +194,13 @@ struct WebEidView: View { Toast.show(errorMessage) } .onChange(of: webEidUrl) {_, url in - if let host = url.host { - - switch host { - case "auth": - viewModel.handleAuth(url: url) - case "cert": - viewModel.handleCertificate(url: url) - case "sign": - viewModel.handleSign(url: url) - default: - viewModel.handleUnknown(url: url) - } - } + handleWebEidOperation(for: url) } .onChange(of: viewModel.authRequest) {_, _ in - Task { - if await viewModel.isWebEidSessionActive() { - await nfcViewModel.clearTempCAN() - } - await viewModel.setWebEidSessionActive(true) - } + activateWebEidSession() } .onChange(of: viewModel.certRequest) {_, _ in - Task { - if await viewModel.isWebEidSessionActive() { - await nfcViewModel.clearTempCAN() - } - await viewModel.setWebEidSessionActive(true) - } + activateWebEidSession() } } } diff --git a/RIADigiDoc/Util/WebEid/WebEidUriUtil.swift b/RIADigiDoc/Util/WebEid/WebEidUriUtil.swift new file mode 100644 index 00000000..d38bc1bf --- /dev/null +++ b/RIADigiDoc/Util/WebEid/WebEidUriUtil.swift @@ -0,0 +1,62 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation + +public enum WebEidOperation: String, CaseIterable, Sendable { + case auth + case cert + case sign + case unknown + + public static func fromOperation(_ operation: String) -> WebEidOperation { + Self.allCases.first { $0.rawValue == operation } ?? WebEidOperation.unknown + } +} + +public enum WebEidUriUtil { + private static let customScheme = "web-eid-mobile" + private static let appLinksHost = "riadigidoc.ee" + + public static func isWebEidUri(_ url: URL) -> Bool { + getOperation(from: url) != WebEidOperation.unknown + } + + public static func getOperation(from url: URL) -> WebEidOperation { + var operation: String? + + #if DEBUG + let isCustomSchemeMatch = url.scheme == customScheme + #else + let isCustomSchemeMatch = false + #endif + + if isCustomSchemeMatch { + operation = url.host + } else if url.scheme == "https", url.host == appLinksHost { + operation = url.pathComponents.dropFirst().first + } else { + operation = WebEidOperation.unknown.rawValue + } + + + guard let operation else { return WebEidOperation.unknown } + return WebEidOperation.fromOperation(operation) + } +} diff --git a/RIADigiDocTests/Util/WebEid/WebEidUriUtilTests.swift b/RIADigiDocTests/Util/WebEid/WebEidUriUtilTests.swift new file mode 100644 index 00000000..d8b6e229 --- /dev/null +++ b/RIADigiDocTests/Util/WebEid/WebEidUriUtilTests.swift @@ -0,0 +1,129 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +import Foundation +import Testing + +struct WebEidUriUtilTests { + + private func makeURL(_ string: String) -> URL { + URL(string: string)! + } + + @Test + func isWebEidUri_appLinks_auth() { + #expect(WebEidUriUtil.isWebEidUri(makeURL("https://riadigidoc.ee/auth"))) + } + + @Test + func isWebEidUri_appLinks_cert() { + #expect(WebEidUriUtil.isWebEidUri(makeURL("https://riadigidoc.ee/cert"))) + } + + @Test + func isWebEidUri_appLinks_sign() { + #expect(WebEidUriUtil.isWebEidUri(makeURL("https://riadigidoc.ee/sign"))) + } + + @Test + func isWebEidUri_appLinks_unknownOperation() { + #expect(!WebEidUriUtil.isWebEidUri(makeURL("https://riadigidoc.ee/unknown"))) + } + + @Test + func isWebEidUri_wrongHost() { + #expect(!WebEidUriUtil.isWebEidUri(makeURL("https://evil.com/auth"))) + } + + @Test + func isWebEidUri_contentScheme() { + #expect(!WebEidUriUtil.isWebEidUri(makeURL("content://some/path"))) + } + + @Test + func isWebEidUri_fileScheme() { + #expect(!WebEidUriUtil.isWebEidUri(makeURL("file:///some/path"))) + } + + @Test + func getOperation_appLinks_auth() { + #expect(WebEidUriUtil.getOperation(from: makeURL("https://riadigidoc.ee/auth#dGVzdA")) == .auth) + } + + @Test + func getOperation_appLinks_cert() { + #expect(WebEidUriUtil.getOperation(from: makeURL("https://riadigidoc.ee/cert#dGVzdA")) == .cert) + } + + @Test + func getOperation_appLinks_sign() { + #expect(WebEidUriUtil.getOperation(from: makeURL("https://riadigidoc.ee/sign#dGVzdA")) == .sign) + } + + @Test + func getOperation_appLinks_unknownOperation_returnsNil() { + #expect(WebEidUriUtil.getOperation(from: makeURL("https://riadigidoc.ee/unknown")) == .unknown) + } + + @Test + func getOperation_unrelatedUri_returnsNil() { + #expect(WebEidUriUtil.getOperation(from: makeURL("https://example.com/auth")) == .unknown) + } + + @Test + func isWebEidUri_customScheme_auth() { + #expect(WebEidUriUtil.isWebEidUri(makeURL("web-eid-mobile://auth"))) + } + + @Test + func isWebEidUri_customScheme_cert() { + #expect(WebEidUriUtil.isWebEidUri(makeURL("web-eid-mobile://cert"))) + } + + @Test + func isWebEidUri_customScheme_sign() { + #expect(WebEidUriUtil.isWebEidUri(makeURL("web-eid-mobile://sign"))) + } + + @Test + func isWebEidUri_customScheme_unknownOperation() { + #expect(!WebEidUriUtil.isWebEidUri(makeURL("web-eid-mobile://unknown"))) + } + + @Test + func getOperation_customScheme_auth() { + #expect(WebEidUriUtil.getOperation(from: makeURL("web-eid-mobile://auth#dGVzdA")) == .auth) + } + + @Test + func getOperation_customScheme_cert() { + #expect(WebEidUriUtil.getOperation(from: makeURL("web-eid-mobile://cert#dGVzdA")) == .cert) + } + + @Test + func getOperation_customScheme_sign() { + #expect(WebEidUriUtil.getOperation(from: makeURL("web-eid-mobile://sign#dGVzdA")) == .sign) + } + + @Test + func getOperation_unknownOperation_returnsNil() { + #expect(WebEidUriUtil.getOperation(from: makeURL("web-eid-mobile://unknown")) == .unknown) + } +}