Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/Threema/Core/URLHandler/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 27 kB image not shown  

Quelle  URLHandler.swift   Sprache: JAVA

 

import CocoaLumberjackSwift
import CommonCrypto
import SwiftUI
import ThreemaEssentials
import ThreemaFramework
import ThreemaMacros

final class URLHandler {
    
    private let businessInjector: any BusinessInjectorProtocol
    
    // MARK: Lifecycle

    required init(businessInjector: any BusinessInjectorProtocol) {
        self.businessInjector = businessInjector
    }
    
    convenience init() {
        self.init(businessInjector: BusinessInjector.ui)
    }
    
    // MARK: Public functions
    
    /// Function to handle the URL
    /// - Parameters:
    ///   - url: URL to handle
    ///   - hideAppChooser: Boolean to hide the app chooser if there are other targets installed. Default is `false`
    func handle(_ url: URL, hideAppChooser: Bool = false) {
        switch URLHandlerMapping.map(url) {
        case let .threemaDotID(url, targetIdentity):
            handleThreemaDotIDURL(
                url: url,
                targetIdentity: targetIdentity,
                hideAppChooser: hideAppChooser
            )
            
        case let .linkMobileNoCode(code):
            handleLinkMobileNo(code: code)
            
        case let .restore(query):
            Task { @MainActor [weak self] in
                self?.handleRestore(query)
            }
            
        case let .compose(query):
            handleCompose(query: query)
            
        case let .addIdentity(targetIdentity, query):
            addIdentity(targetIdentity, compose: query["text"] != nil, query: query)
            
        case let .license(query):
            URLHandler.handleLicense(licenseStore: businessInjector.licenseStore, query: query)
            
        case let .file(url):
            handleFile(url: url)
            
        case let .threemaLink(url):
            if UIApplication.shared.canOpenURL(url) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            }
            
        case .unknown:
            DDLogError(
                "[URLHandler] Failed to handle URL: Unknown host (\(url.host() ?? "unknown")). Aborting URL handling."
            )
        }
    }
    
    /// Handle a license activation link outside of onboarding, without a business injector
    ///
    /// Used by ``RootCoordinator`` for links that arrive while the launch sequence runs, so before
    /// the initialization of remote secret and the business injector.
    /// - Parameter url: URL to handle
    static func handleLicense(from url: URL) {
        guard url.host()?.lowercased() == "license" else {
            DDLogError(
                "[URLHandler] Failed to handle URL: Not a license URL (\(url.host() ?? "unknown")). Aborting license handling."
            )
            return
        }
        handleLicense(licenseStore: LicenseStore.shared(), query: url.getQueries)
    }
    
    // MARK: Private functions

    private lazy var otherThreemaAppsInstalled: [String: String] = {
        var installedAppsList = [String: String]()
        let current = TargetManager.current
        
        guard current != .customOnPrem else {
            return installedAppsList
        }
        
        if current == .work || current == .onPrem,
           let url = AppURLScheme.threema.url(),
           UIApplication.shared.canOpenURL(url) {
            installedAppsList[AppURLScheme.threema.rawValue] = url.absoluteString
        }
        if current == .threema || current == .onPrem,
           let url = AppURLScheme.threemaWork.url(),
           UIApplication.shared.canOpenURL(url) {
            installedAppsList[AppURLScheme.threemaWork.rawValue] = url.absoluteString
        }
        if current == .threema || current == .work,
           let url = AppURLScheme.threemaOnPrem.url(),
           UIApplication.shared.canOpenURL(url) {
            installedAppsList[AppURLScheme.threemaOnPrem.rawValue] = url.absoluteString
        }
        if current == .green,
           let url = AppURLScheme.threemaBlue.url(),
           UIApplication.shared.canOpenURL(url) {
            installedAppsList[AppURLScheme.threemaBlue.rawValue] = url.absoluteString
        }
        if current == .blue,
           let url = AppURLScheme.threemaGreen.url(),
           UIApplication.shared.canOpenURL(url) {
            installedAppsList[AppURLScheme.threemaGreen.rawValue] = url.absoluteString
        }
        
        return installedAppsList
    }()
}

// MARK: - URLHandler.URLHandlerMapping

extension URLHandler {
    /// URLHandlerMapping
    private enum URLHandlerMapping {
        case threemaDotID(url: URL, targetIdentity: ThreemaIdentity?)
        case linkMobileNoCode(code: String)
        case restore(query: String)
        case compose(query: [String: String])
        case addIdentity(targetIdentity: ThreemaIdentity, query: [String: String])
        case license(query: [String: String])
        case file(url: URL)
        case threemaLink(url: URL)
        case unknown
        
        /// Retrieve the URL mapping for the provided URL.
        /// - Parameter url: URL
        /// - Returns: URLHandlerMapping
        static func map(_ url: URL) -> URLHandlerMapping {
            switch url.scheme {
            case TargetManager.appURLScheme:
                mapAppURLSchemeFrom(url)
                
            case "file":
                .file(url: url)
                
            case "http", "https":
                mapLink(url)
                
            default:
                .unknown
            }
        }
        
        /// Map the host of the App URL Scheme
        /// - Parameter url: URL
        /// - Returns: URLHandlerMapping
        private static func mapAppURLSchemeFrom(_ url: URL) -> URLHandlerMapping {
            switch url.host {
            case "link_mobileno":
                guard let query = url.query(percentEncoded: true) else {
                    DDLogError(
                        "[URLHandler] Failed to handle URL: Can't start link mobile number when query is empty. Aborting link mobile number handling."
                    )
                    return .unknown
                }
                return .linkMobileNoCode(code: query.replacingOccurrences(of: "code=", with: ""))
                
            case "restore":
                guard let query = url.query(percentEncoded: true) else {
                    DDLogError(
                        "[URLHandler] Failed to handle URL: Can't start restore when query is empty. Aborting restore identity handling."
                    )
                    return .unknown
                }
                return .restore(query: query)
                
            case "compose":
                let query = url.getQueries
                
                if let identity = query["id"]?.uppercased(),
                   identity.count == ThreemaIdentity.length {
                    return .addIdentity(targetIdentity: ThreemaIdentity(rawValue: identity), query: query)
                }
                
                return .compose(query: query)
                
            case "add":
                let query = url.getQueries
                
                guard let identity = query["id"]?.uppercased(),
                      identity.count == ThreemaIdentity.length else {
                    return .compose(query: query)
                }
                
                return .addIdentity(targetIdentity: ThreemaIdentity(rawValue: identity), query: query)
                
            case "license":
                let query = url.getQueries
                return .license(query: query)
                
            case .none:
                return .unknown
                
            case .some:
                return .unknown
            }
        }
        
        /// Map the host of the HTTP URL
        /// - Parameter url: URL
        /// - Returns: URLHandlerMapping
        private static func mapLink(_ url: URL) -> URLHandlerMapping {
            switch url.host()?.lowercased() {
            case "threema.id":
                if url.lastPathComponent.count == ThreemaIdentity.length {
                    .threemaDotID(url: url, targetIdentity: ThreemaIdentity(rawValue: url.lastPathComponent))
                }
                else {
                    mapComposeActionsFrom(url)
                }
                
            case "threema.ch", "threema.com":
                .threemaLink(url: url)
                
            default:
                .unknown
            }
        }
        
        private static func mapComposeActionsFrom(_ url: URL) -> URLHandlerMapping {
            guard url.lastPathComponent.lowercased() == "compose" else {
                return .unknown
            }

            let query = url.getQueries

            // Route through `.threemaDotID` so compose links present the same "Open in …" app
            // chooser as `threema.id/<IDENTITY>` links. `handleThreemaDotIDURL` falls back to a plain
            // compose when there is no target identity.
            guard
                let identity = query["id"]?.uppercased(),
                identity.count == ThreemaIdentity.length
            else {
                return .threemaDotID(url: url, targetIdentity: nil)
            }

            return .threemaDotID(url: url, targetIdentity: ThreemaIdentity(rawValue: identity))
        }
    }
}

// MARK: Handle Private Functions

extension URLHandler {
    /// Open the ShareController with the provided text or image.
    /// - Parameter query: The query containing the text and image if provided.
    private func handleCompose(query: [String: String]?) {
        DDLogVerbose("[URLHandler] Handle compose")
        
        // Share with unspecified contact
        Task { @MainActor in
            let shareController = ShareController()
            if let query {
                shareController.text = query["text"]
            }
            shareController.startShare()
        }
    }
    
    /// Present the ID backup restore with the provided backup key.
    /// - Parameter query: The query containing the backup key.
    @MainActor
    private func handleRestore(_ query: String) {
        DDLogVerbose("[URLHandler] Handle restore")
        
        // only react to restore URLs if we're currently presenting the generate key view controller
        guard SharedAppProvider.isPresentingKeyGeneration else {
            DDLogError(
                "[URLHandler] Failed to handle URL: Can't start restore when app is not presenting the setup screen. Aborting restore identity handling."
            )
            return
        }
        
        RestoreDataURLProvider.urlString = query.replacingOccurrences(of: "backup=", with: "")
        SharedAppProvider.presentIDBackupRestore()
    }
    
    /// Function to validate the provided mobile number
    /// - Parameter code: Confirmation code
    private func handleLinkMobileNo(code: String) {
        DDLogVerbose("[URLHandler] Link mobile code: \(code)")
        let connection = ServerAPIConnector()
        
        connection.linkMobileNo(with: MyIdentityStore.shared(), code: code) { _ in
            NotificationPresenterWrapper.shared.present(type: .linkMobileNoSuccess)
        } onError: { _ in
            DDLogError(
                "[URLHandler] Failed to handle URL: Link mobile number failed. Aborting link mobile number handling."
            )
            NotificationPresenterWrapper.shared.present(type: .linkMobileNoFailed)
        }
    }
    
    /// Handle the .threemaID URL
    /// - Parameters:
    ///   - url: URL
    ///   - targetIdentity: Targeted Identity
    ///   - hideAppChooser: Whether to hide the app chooser
    private func handleThreemaDotIDURL(url: URL, targetIdentity: ThreemaIdentity?, hideAppChooser: Bool) {
        DDLogVerbose("[URLHandler] Handle Threema.id URL: \(url)")
        
        guard !TargetManager.isCustomOnPrem else {
            DDLogError(
                "[URLHandler] Failed to handle URL: DotThreemaID is not supported in the current flavor. Aborting DotThreemaID handling."
            )
            return
        }
        
        guard !otherThreemaAppsInstalled.isEmpty, !hideAppChooser else {
            let query = url.getQueries
            
            if let targetIdentity {
                let compose = query["text"] != nil || query["image"] != nil
                addIdentity(targetIdentity, compose: compose, query: query)
            }
            else {
                handleCompose(query: query)
            }
            return
        }
        
        handleAppChooser(url: url, targetIdentity: targetIdentity)
    }
    
    /// Display the application chooser with the installed targets
    /// - Parameters:
    ///   - url: URL
    ///   - targetIdentity: Targeted Identity
    private func handleAppChooser(url: URL, targetIdentity: ThreemaIdentity?) {
        let query = url.getQueries

        Task { @MainActor in
            let alertController = UIAlertController(
                title: #localize("Open in ..."),
                message: url.absoluteString,
                preferredStyle: .alert
            )

            alertController.addAction(UIAlertAction(title: TargetManager.appName, style: .default) { _ in
                self.openURLInCurrentApp(targetIdentity: targetIdentity, query: query)
            })

            for app in otherThreemaAppsInstalled {
                alertController.addAction(UIAlertAction(title: app.key, style: .default) { _ in
                    self.openURLInOtherTargetApp(targetAppURL: app.value, url: url, targetIdentity: targetIdentity)
                })
            }

            URLHandler.topViewController.present(alertController, animated: true)
        }
    }
    
    /// Handle the URL for the current application target
    /// - Parameters:
    ///   - targetIdentity: Targeted Identity
    ///   - query: The query containing the text or image if provided.
    private func openURLInCurrentApp(targetIdentity: ThreemaIdentity?, query: [String: String]) {
        if let targetIdentity {
            addIdentity(
                targetIdentity,
                compose: query["text"] != nil,
                query: query
            )
        }
        else {
            handleCompose(query: query)
        }
    }
    
    /// Construct and open the URL for the other application target
    /// - Parameters:
    ///   - targetAppURL: URL of the other target (e.g. `threema://app`)
    ///   - url: The URL
    ///   - targetIdentity: The Targeted Identity
    private func openURLInOtherTargetApp(targetAppURL: String, url: URL, targetIdentity: ThreemaIdentity?) {
        guard var components = URLComponents(string: targetAppURL) else {
            DDLogError("[URLHandler] Failed to handle URL: \(targetAppURL) is not a valid URL")
            return
        }

        var queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []

        if let targetIdentity {
            // Without any other parameter the identity is just added, otherwise a message is composed for it
            components.host = queryItems.isEmpty ? "add" : "compose"
            queryItems.insert(URLQueryItem(name: "id", value: targetIdentity.rawValue), at: 0)
        }
        else {
            components.host = "compose"
        }

        components.queryItems = queryItems.isEmpty ? nil : queryItems

        guard let newURL = components.url else {
            DDLogError("[URLHandler] Failed to handle URL: Unable to compose URL for \(targetAppURL)")
            return
        }

        DDLogVerbose("Open in \(newURL.absoluteString) (\(url.absoluteString))")
        UIApplication.shared.open(newURL)
    }
    
    /// Display the ShareController with the provided URL.
    /// - Parameter url: The URL
    private func handleFile(url: URL) {
        DDLogVerbose("[URLHandler] Handle file: \(url)")
        let shareController = ShareController()
        shareController.url = url
        
        Task { @MainActor [weak shareController] in
            shareController?.startShare()
        }
    }
    
    /// Handle and verify the provided license.
    ///
    /// This is the path of an app that is already set up. While onboarding runs, license links are
    /// handled by ``OnboardingCoordinator``, see ``RootCoordinator.handleLicenseURL(_:)``.
    /// - Parameter licenseStore: Store to handle license in
    /// - Parameter query: Query containing username, password, and server information if provided.
    private static func handleLicense(licenseStore: any LicenseStoreProtocol, query: [String: String]) {
        DDLogVerbose("[URLHandler] Handle license: \(query)")

        guard TargetManager.isBusinessApp else {
            DDLogError("[URLHandler] Failed to handle URL: No license for private app. Aborting license handling.")
            return
        }

        Task { @MainActor in
            guard !licenseStore.isValid() else {
                UIAlertTemplate.showOKAlert(
                    owner: topViewController,
                    title: #localize("already_licensed"),
                    message: nil,
                    actionOk: nil
                )
                DDLogError("[URLHandler] Failed to handle URL: App is already licensed. Aborting license handling.")
                return
            }

            guard let mdmSetup = MDMSetup() else {
                DDLogError("[URLHandler] Failed to handle URL: Missing MDM Setup. Aborting license handling.")
                return
            }

            let applyResult = LicenseCredentials(query: query).apply(
                to: licenseStore,
                mdmSetup: mdmSetup,
                isOnPrem: TargetManager.isOnPrem
            )

            guard applyResult == .complete else {
                NotificationCenter.default.post(
                    name: Notification.Name(rawValue: kNotificationLicenseMissing),
                    object: applyResult == .invalidServer
                        ? #localize("enter_license_invalid_url_link")
                        : nil
                )
                DDLogError(
                    "[URLHandler] Failed to handle URL: License link is \(applyResult). Aborting license handling."
                )
                return
            }

            await performLicenseCheck(with: licenseStore)
        }
    }

    /// Verify the credentials in the license store and, if the license entry screen is presented,
    /// dismiss it and reconnect.
    /// - Parameter licenseStore: LicenseStore
    @MainActor
    private static func performLicenseCheck(with licenseStore: any LicenseStoreProtocol) async {
        guard await licenseStore.performLicenseCheck() else {
            NotificationCenter.default.post(
                name: Notification.Name(rawValue: kNotificationLicenseMissing),
                object: nil
            )
            DDLogError("[URLHandler] Failed to handle URL: License is missing. Aborting perform license check.")
            return
        }

        do {
            try await WorkDataThreemaMDMFetcher(licenseStore: licenseStore).checkUpdateThreemaMDM()
        }
        catch {
            DDLogError("[URLHandler] Failed to handle URL: Threema MDM fetch failed (\(error)).")
            NotificationCenter.default.post(
                name: Notification.Name(rawValue: kNotificationLicenseMissing),
                object: nil
            )
            return
        }

        guard SharedAppProvider.isPresentingEnterLicense else {
            // The app is licensed again and there is no license entry screen to dismiss
            DDLogNotice("[URLHandler] License is valid and license screen is not presented.")
            return
        }

        SharedAppProvider.window?.rootViewController?.dismiss(animated: true) {
            Task { @MainActor in
                // The app coordinator is gone if the app isn't set up (yet); the license
                // credentials are stored, so the next launch connects with them.
                SharedAppProvider.appCoordinator?.setupConnection()
            }
        }
    }
    
    /// Verify the existence of a contact and add the specified identity to the database.
    /// - Parameters:
    ///   - targetIdentity: The targeted identity to be added.
    ///   - compose: Whether to compose a message to this contact.
    ///   - query: The query with the text or image if provided.
    private func addIdentity(_ targetIdentity: ThreemaIdentity, compose: Bool, query: [String: String]) {
        DDLogVerbose("[URLHandler] Add Identity: \(targetIdentity), compose: \(compose), query: \(query)")
        
        guard targetIdentity.rawValue != businessInjector.profileStore.profile.myIdentity.rawValue else {
            DDLogError("[URLHandler] Failed to handle URL: Can't add own identity. Aborting add identity handling.")
            return
        }
        
        // First, let’s check if MDMSetup is available (not for private flavors) and make sure the ‘add contacts’ option
        // is disabled. Then, we’ll double-check that this contact already exists because we can’t add any new ones.
        if let mdmSetup = MDMSetup(),
           mdmSetup.disableAddContact() {
            guard businessInjector.entityManager.entityFetcher.contactEntity(for: targetIdentity.rawValue) != nil else {
                DDLogError(
                    "[URLHandler] Failed to handle URL: Can't add contact because it's disabled. Aborting add identity handling."
                )
                return
            }
        }
        
        businessInjector.contactStore.addContact(
            with: targetIdentity.rawValue,
            verificationLevel: Int32(VerificationLevel.unverified.rawValue),
            onCompletion: { contactEntity, _ in
                if compose,
                   query["text"] != nil || query["image"] != nil {
                    Task { @MainActor in
                        let shareController = ShareController()
                        shareController.contact = contactEntity as? ContactEntity
                        shareController.text = query["text"]
                        shareController.startShare()
                    }
                }
                else {
                    // just show contact details
                    let dict = [kKeyContact: contactEntity as Any, "fromURL": true]
                    Task {
                        NotificationCenter.default.post(
                            name: Notification.Name(kNotificationShowContact),
                            object: nil,
                            userInfo: dict
                        )
                    }
                }
            }, onError: { _ in
                NotificationPresenterWrapper.shared.present(type: .addContactFailed)
                DDLogError("[URLHandler] Failed to handle URL: Add contact failed. Aborting add identity handling.")
            }
        )
    }
}

// MARK: ShortcutItems

extension URLHandler {
    @MainActor
    func handle(item: UIApplicationShortcutItem) -> Bool {
        if item.type == "ch.threema.newmessage" {
            handleCompose(query: nil)
            return true
        }
        else if item.type == "ch.threema.myid" {
            guard let tabBar = SharedAppProvider.tabBarController else {
                return false
            }
            tabBar.selectedIndex = Int(kMyIdentityTabBarIndex)
            return true
        }
        else if item.type == "ch.threema.scanid" {
            guard let tabBar = SharedAppProvider.tabBarController else {
                return false
            }
            if !DeviceCapabilitiesManager().hasVideoCaptureDevice {
                DDLogVerbose("No Camera available.")
                return false
            }
            if TargetManager.isBusinessApp, MDMSetup()?.disableAddContact() == true {
                DDLogVerbose("Contact scanning is disabled for this business app.")
                return false
            }
            
            let model = QRCodeScannerViewModel(
                mode: .identity,
                audioSessionManager: BusinessInjector.ui.audioSessionManager,
                systemFeedbackManager: SystemFeedbackManager(
                    deviceCapabilitiesManager: DeviceCapabilitiesManager(),
                    settingsStore: BusinessInjector.ui.settingsStore
                ),
                systemPermissionsManager: SystemPermissionsManager()
            )
            model.onCompletion = { result in
                self.handleScannerResult(result)
            }
            model.onCancel = {
                tabBar.dismiss(animated: true)
            }
            let rootView = QRCodeScannerView(model: model)
            let viewController = UIHostingController(rootView: rootView)
            let nav = PortraitNavigationController(rootViewController: viewController)
            URLHandler.topViewController.present(nav, animated: true)
            
            return true
        }
        else {
            DDLogError("[URLHandler] Unknown shortcut type: (\(item.type)). Aborting shortcut handling.")
            return false
        }
    }
}

// MARK: ScannerResult

extension URLHandler {
    private func handleScannerResult(_ result: QRCodeScannerViewModel.QRCodeResult) {
        MainActor.assumeIsolated {
            switch result {
            case let .identityContact(identity: id, publicKey: key, expirationDate: date):
                let model = ContactIdentityProcessingViewModel(
                    expectedIdentity: nil,
                    scannedIdentity: id,
                    scannedPublicKey: key,
                    scannedExpirationDate: date,
                    systemFeedbackManager: SystemFeedbackManager(
                        deviceCapabilitiesManager: DeviceCapabilitiesManager(),
                        settingsStore: BusinessInjector.ui.settingsStore
                    )
                )
                model.onCompletion = { verifiedContact in
                    URLHandler.topViewController.dismiss(animated: true) {
                        if let verifiedContact {
                            let name = Notification.Name(kNotificationShowContact)
                            let userInfo = [kKeyContact: verifiedContact]
                            NotificationCenter.default.post(name: name, object: nil, userInfo: userInfo)
                        }
                    }
                }
                let rootView = ContactIdentityProcessingView(model: model)
                let viewController = UIHostingController(rootView: rootView)
                (URLHandler.topViewController as? UINavigationController)?.pushViewController(
                    viewController,
                    animated: true
                )
                
            case let .identityLink(url: url):
                URLHandler.topViewController.dismiss(animated: true) {
                    self.handle(url, hideAppChooser: true)
                }
                
            default:
                break
            }
        }
    }
}

// MARK: Helper functions

extension URLHandler {
    private enum AppURLScheme: String {
        case threema = "Threema"
        case threemaWork = "Threema Work"
        case threemaOnPrem = "Threema OnPrem"
        case threemaBlue = "Threema Blue"
        case threemaGreen = "Threema Green"
        
        func url() -> URL? {
            switch self {
            case .threema:
                URL(string: "threema://app")
            case .threemaWork:
                URL(string: "threemawork://app")
            case .threemaOnPrem:
                URL(string: "threemaonprem://app")
            case .threemaBlue:
                URL(string: "threemablue://app")
            case .threemaGreen:
                URL(string: "threemagreen://app")
            }
        }
    }
    
    @MainActor
    private static var topViewController: UIViewController {
        SharedAppProvider.currentTopViewController ?? UIViewController()
    }
}

Messung
CHZG
86999793

¤ Dauer der Verarbeitung: 0.9 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.