From 050ce19425590d5b73b3b15154637c56f22e721e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Caetano?= Date: Mon, 26 Nov 2018 18:29:10 -0200 Subject: [PATCH] Fix webview's resource loading detection fix #56 --- .../RxSwift/ReCaptcha+Rx__Tests.swift | 2 +- Example/ReCaptcha_Tests/mock.html | 4 - .../Classes/ReCaptchaWebViewManager.swift | 109 +++--------------- 3 files changed, 19 insertions(+), 96 deletions(-) diff --git a/Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift b/Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift index 8f96b63..2e171d3 100644 --- a/Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift +++ b/Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift @@ -107,7 +107,7 @@ class ReCaptcha_Rx__Tests: XCTestCase { let exp = expectation(description: "stop loading") // Stop - let recaptcha = ReCaptcha(manager: ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}")) + let recaptcha = ReCaptcha(manager: ReCaptchaWebViewManager(messageBody: "{log: \"foo\"}")) recaptcha.configureWebView { _ in XCTFail("should not ask to configure the webview") } diff --git a/Example/ReCaptcha_Tests/mock.html b/Example/ReCaptcha_Tests/mock.html index 4c05cf8..37df0ea 100644 --- a/Example/ReCaptcha_Tests/mock.html +++ b/Example/ReCaptcha_Tests/mock.html @@ -19,10 +19,6 @@ } }; - window.onload = function() { - post({action: "didLoad"}); - }; - var reset = function() { shouldFail = false; post({action: "didLoad"}); diff --git a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift index 4222e38..599dd68 100644 --- a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift +++ b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift @@ -13,88 +13,6 @@ import WebKit /** Handles comunications with the webview containing the ReCaptcha challenge. */ internal class ReCaptchaWebViewManager { - /** The `webView` delegate object that performs execution uppon script loading - */ - fileprivate class WebViewDelegate: NSObject, WKNavigationDelegate { - struct Constants { - /// The host that loaded requests should have - static let apiURLHost = "www.google.com" - } - - /// The parent manager - private weak var manager: ReCaptchaWebViewManager? - - /// The active requests' urls - private var activeRequests = Set(minimumCapacity: 0) - - /// - parameter manager: The parent manager - init(manager: ReCaptchaWebViewManager) { - self.manager = manager - } - - /** - - parameters: - - webView: The web view invoking the delegate method. - - navigationAction: Descriptive information about the action triggering the navigation request. - - decisionHandler: The decision handler to call to allow or cancel the navigation. The argument is one of - the constants of the enumerated type WKNavigationActionPolicy. - - Decides whether to allow or cancel a navigation. - */ - func webView( - _ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy - ) -> Void) { - defer { decisionHandler(.allow) } - - if let url = navigationAction.request.url, url.host == Constants.apiURLHost { - activeRequests.insert(url.absoluteString) - } - } - - /** - - parameters: - - webView: The web view invoking the delegate method. - - navigationResponse: Descriptive information about the navigation response. - - decisionHandler: A block to be called when your app has decided whether to allow or cancel the navigation - - Decides whether to allow or cancel a navigation after its response is known. - */ - func webView( - _ webView: WKWebView, - decidePolicyFor navigationResponse: WKNavigationResponse, - decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void - ) { - defer { decisionHandler(.allow) } - guard let url = navigationResponse.response.url?.absoluteString, - activeRequests.remove(url) != nil, activeRequests.isEmpty else { - return - } - - execute() - } - - /// Flag the requests as finished and call ReCaptcha execution if necessary - func execute() { - guard manager?.didFinishLoading != true else { return } - - DispatchQueue.main.throttle(deadline: .now() + 1, context: self) { [weak self] in - // Did finish loading the ReCaptcha JS source - self?.manager?.didFinishLoading = true - - if self?.manager?.completion != nil { - // User has requested for validation - self?.manager?.execute() - } - } - } - - /// Flags all requests as finished - func reset() { - activeRequests.removeAll() - } - } fileprivate struct Constants { static let ExecuteJSCommand = "execute();" @@ -135,29 +53,39 @@ internal class ReCaptchaWebViewManager { fileprivate var decoder: ReCaptchaDecoder! /// Indicates if the script has already been loaded by the `webView` - fileprivate var didFinishLoading = false // webView.isLoading does not work in this case + fileprivate var didFinishLoading = false { // webView.isLoading does not work for WKWebview.loadHTMLString + didSet { + if didFinishLoading && completion != nil { + // User has requested for validation + // A small delay is necessary to allow JS to wrap its operations and avoid errors. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in + self?.execute() + } + } + } + } /// The observer for `.UIWindowDidBecomeVisible` fileprivate var observer: NSObjectProtocol? + /// The observer for `\WKWebView.estimatedProgress` + fileprivate var loadingObservation: NSKeyValueObservation? + /// The endpoint url being used fileprivate var endpoint: String - /// The `webView` delegate implementation - fileprivate lazy var webviewDelegate: WebViewDelegate = { - WebViewDelegate(manager: self) - }() - /// The webview that executes JS code lazy var webView: WKWebView = { let webview = WKWebView( frame: CGRect(x: 0, y: 0, width: 1, height: 1), configuration: self.buildConfiguration() ) - webview.navigationDelegate = self.webviewDelegate webview.accessibilityIdentifier = "webview" webview.accessibilityTraits = UIAccessibilityTraits.link webview.isHidden = true + self.loadingObservation = webview.observe(\.estimatedProgress, options: [.new]) { [weak self] _, change in + self?.didFinishLoading = change.newValue == 1 + } return webview }() @@ -224,7 +152,6 @@ internal class ReCaptchaWebViewManager { func reset() { didFinishLoading = false configureWebViewDispatchToken = UUID() - webviewDelegate.reset() webView.evaluateJavaScript(Constants.ResetCommand) { [weak self] _, error in if let error = error { @@ -297,7 +224,7 @@ fileprivate extension ReCaptchaWebViewManager { case .didLoad: // For testing purposes - webviewDelegate.execute() + didFinishLoading = true case .log(let message): #if DEBUG