Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalUI/Views/CaptchaView.swift
1 views
//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import SignalServiceKit
public import WebKit

public protocol CaptchaViewDelegate: NSObjectProtocol {
    func captchaView(_: CaptchaView, didCompleteCaptchaWithToken: String)
    func captchaViewDidFailToCompleteCaptcha(_: CaptchaView)
}

public enum CaptchaContext {
    case registration
    case challenge

    fileprivate var url: URL {
        switch self {
        case .registration:
            return URL(string: TSConstants.registrationCaptchaURL)!
        case .challenge:
            return URL(string: TSConstants.challengeCaptchaURL)!
        }
    }
}

public class CaptchaView: UIView {

    private let context: CaptchaContext

    public init(context: CaptchaContext) {
        self.context = context

        super.init(frame: .zero)

        addSubview(webView)
        webView.autoPinEdgesToSuperviewEdges()

        webView.navigationDelegate = self

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(didBecomeActive),
            name: .OWSApplicationDidBecomeActive,
            object: nil,
        )
    }

    private var webView: WKWebView = {
        // We want the CAPTCHA web content to "fill the screen (honoring margins)".
        // The way to do this with WKWebView is to inject a javascript snippet that
        // manipulates the viewport.
        //
        // TODO: There's a long outstanding where short devices will require vertical
        // scrolling to see the entire captcha. We should manipulate the viewport to mitigate
        // this.
        let viewportInjection = WKUserScript(
            source: """
            var meta = document.createElement('meta');
            meta.setAttribute('name', 'viewport');
            meta.setAttribute('content', 'width=device-width');
            document.getElementsByTagName('head')[0].appendChild(meta);
            """,
            injectionTime: .atDocumentEnd,
            forMainFrameOnly: true,
        )

        let contentController = WKUserContentController()
        contentController.addUserScript(viewportInjection)
        let configuration = WKWebViewConfiguration()
        configuration.websiteDataStore = .nonPersistent()
        configuration.userContentController = contentController

        let webView = WKWebView(frame: .zero, configuration: configuration)
        webView.allowsBackForwardNavigationGestures = false
        webView.allowsLinkPreview = false
        webView.scrollView.contentInset = .zero
        webView.layoutMargins = .zero
        webView.accessibilityIdentifier = "onboarding.captcha." + "webView"
        return webView
    }()

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public weak var delegate: CaptchaViewDelegate?

    public func loadCaptcha() {
        webView.load(URLRequest(url: context.url))
    }

    @objc
    private func didBecomeActive() {
        loadCaptcha()
    }

    // Example URL:
    // signalcaptcha://03AF6jDqXgf1PocNNrWRJEENZ9l6RAMIsUoESi2dFKkxTgE2qjdZGVjE
    // W6SZNFQqeRRTgGqOii6zHGG--uLyC1HnhSmRt8wHeKxHcg1hsK4ucTusANIeFXVB8wPPiV7U
    // _0w2jUFVak5clMCvW9_JBfbfzj51_e9sou8DYfwc_R6THuTBTdpSV8Nh0yJalgget-nSukCx
    // h6FPA6hRVbw7lP3r-me1QCykHOfh-V29UVaQ4Fs5upHvwB5rtiViqT_HN8WuGmdIdGcaWxaq
    // y1lQTgFSs2Shdj593wZiXfhJnCWAw9rMn3jSgIZhkFxdXwKOmslQ2E_I8iWkm6
    private func parseCaptchaResult(url: URL) {
        if let token = url.host, !token.isEmpty {
            delegate?.captchaView(self, didCompleteCaptchaWithToken: token)
        } else {
            owsFailDebug("Could not parse captcha token: \(url)")
            delegate?.captchaViewDidFailToCompleteCaptcha(self)
        }
    }
}

extension CaptchaView: WKNavigationDelegate {
    public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
        guard let url: URL = navigationAction.request.url else {
            owsFailDebug("Missing URL.")
            return .cancel
        }

        if url.scheme == "signalcaptcha" {
            parseCaptchaResult(url: url)
            return .cancel
        }

        // Loading the Captcha content involves a series of actions.
        return .allow
    }

    public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy {
        .allow
    }

    public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
        delegate?.captchaViewDidFailToCompleteCaptcha(self)
    }

    public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        delegate?.captchaViewDidFailToCompleteCaptcha(self)
    }

    public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
        delegate?.captchaViewDidFailToCompleteCaptcha(self)
    }
}