Compare commits

..

No commits in common. "master" and "1.4.1" have entirely different histories.

12 changed files with 109 additions and 137 deletions

View File

@ -1,13 +1,14 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
--- ---
<!-- <!--
## Is it really a bug? ## Is it really a bug?
Before opening an issue, check the following: Before opening an issue, check the following:
1. You are using the **SITE** key 1. You are using the **Client side integration** key
2. The correct domain, with protocol, is setup. 2. The correct domain, with protocol, is setup.
3. You are using an **Invisible** reCAPTCHA v2 key. 3. You are using an **Invisible** reCAPTCHA key.
4. If the widget doesn't appear, that is expected since the library will try to resolve the challenge _invisibly_. 4. If the widget doesn't appear, that is expected since the library will try to resolve the challenge _invisibly_.
https://www.google.com/recaptcha/admin#site https://www.google.com/recaptcha/admin#site
@ -19,7 +20,7 @@ A clear and concise description of what the bug is.
## To Reproduce ## To Reproduce
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '...' 2. Click on '....'
3. ... 3. ...
4. Profit (jk See error) 4. Profit (jk See error)

View File

@ -1,7 +1,3 @@
# 1.4.2
- Fix: Webview's resource loading detection (#56 #60)
# 1.4.1 # 1.4.1
- Fix RxSwift dependency version (#58) - Fix RxSwift dependency version (#58)

View File

@ -1 +1 @@
binary "https://raw.github.com/TouchInstinct/CarthageBinaries/master/RxSwift/RxSwift.json" github "ReactiveX/RxSwift" ~> 4.3

View File

@ -1 +1 @@
binary "https://raw.github.com/TouchInstinct/CarthageBinaries/master/RxSwift/RxSwift.json" "4.5.0" github "ReactiveX/RxSwift" "4.4.0"

View File

@ -107,7 +107,7 @@ class ReCaptcha_Rx__Tests: XCTestCase {
let exp = expectation(description: "stop loading") let exp = expectation(description: "stop loading")
// Stop // Stop
let recaptcha = ReCaptcha(manager: ReCaptchaWebViewManager(messageBody: "{log: \"foo\"}")) let recaptcha = ReCaptcha(manager: ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}"))
recaptcha.configureWebView { _ in recaptcha.configureWebView { _ in
XCTFail("should not ask to configure the webview") XCTFail("should not ask to configure the webview")
} }

View File

@ -19,6 +19,10 @@
} }
}; };
window.onload = function() {
post({action: "didLoad"});
};
var reset = function() { var reset = function() {
shouldFail = false; shouldFail = false;
post({action: "didLoad"}); post({action: "didLoad"});

View File

@ -9,7 +9,7 @@
----- -----
Add Google's [Invisible ReCaptcha v2](https://developers.google.com/recaptcha/docs/invisible) to your project. This library Add Google's [Invisible ReCaptcha](https://developers.google.com/recaptcha/docs/invisible) to your project. This library
automatically handles ReCaptcha's events and retrieves the validation token or notifies you to present the challenge if automatically handles ReCaptcha's events and retrieves the validation token or notifies you to present the challenge if
invisibility is not possible. invisibility is not possible.
@ -17,15 +17,8 @@ invisibility is not possible.
#### _Warning_ ⚠️ #### _Warning_ ⚠️
Beware that this library only works for ReCaptcha v2 Invisible keys! Make sure to check the reCAPTCHA Beware that this library only works for Invisible ReCaptcha keys! Make sure to check the Invisible reCAPTCHA option
v2 Invisible badge option when creating your [API Key](https://www.google.com/recaptcha/admin/create). when creating your [API Key](https://www.google.com/recaptcha/admin).
![ReCaptcha v2 invisible key example](https://raw.githubusercontent.com/fjcaetano/ReCaptcha/master/example-v2-key.png)
You won't be able to use a ReCaptcha v3 key because it requires server-side validation. On v3, all
challenges succeed into a token which is then validated in the server for a score. For this reason,
a frontend app can't know on its own wether or not a user is valid since the challenge will always
result in a valid token.
## Installation ## Installation
@ -49,7 +42,7 @@ extension for the ReCaptcha framework.
## Usage ## Usage
Simply add `ReCaptchaKey` and `ReCaptchaDomain` (with a protocol ex. http:// or https://) to your Info.plist and run: Simply add `ReCaptchaKey` and `ReCaptchaDomain` (with a protocol) to your Info.plist and run:
``` swift ``` swift
let recaptcha = try? ReCaptcha() let recaptcha = try? ReCaptcha()

View File

@ -1,9 +1,9 @@
Pod::Spec.new do |s| Pod::Spec.new do |s|
s.name = 'ReCaptcha' s.name = 'ReCaptcha'
s.version = '1.4.2' s.version = '1.4.1'
s.summary = 'ReCaptcha for iOS' s.summary = 'ReCaptcha for iOS'
s.swift_version = '4.2' s.swift_version = '4.2'
s.description = <<-DESC s.description = <<-DESC
Add Google's [Invisible ReCaptcha](https://developers.google.com/recaptcha/docs/invisible) to your project. This library Add Google's [Invisible ReCaptcha](https://developers.google.com/recaptcha/docs/invisible) to your project. This library

View File

@ -13,8 +13,6 @@ import WebKit
/** /**
*/ */
public class ReCaptcha { public class ReCaptcha {
public typealias BoolParameterClosure = (Bool) -> ()
fileprivate struct Constants { fileprivate struct Constants {
struct InfoDictKeys { struct InfoDictKeys {
static let APIKey = "ReCaptchaKey" static let APIKey = "ReCaptchaKey"
@ -103,12 +101,6 @@ public class ReCaptcha {
} }
} }
/// Callback for WebView loading state changing
public var onLoadingChanged: BoolParameterClosure? {
get { return manager.onLoadingChanged }
set { manager.onLoadingChanged = newValue }
}
/// The worker that handles webview events and communication /// The worker that handles webview events and communication
let manager: ReCaptchaWebViewManager let manager: ReCaptchaWebViewManager

View File

@ -13,18 +13,93 @@ import WebKit
/** Handles comunications with the webview containing the ReCaptcha challenge. /** Handles comunications with the webview containing the ReCaptcha challenge.
*/ */
internal class ReCaptchaWebViewManager { 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<String>(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 { fileprivate struct Constants {
static let ExecuteJSCommand = "execute();" static let ExecuteJSCommand = "execute();"
static let ResetCommand = "reset();" static let ResetCommand = "reset();"
static let BotUserAgent = "Googlebot/2.1" static let BotUserAgent = "Googlebot/2.1"
static let NumberOfDivsCommand = "document.getElementsByTagName(\"div\").length"
static let MinNumberOfDivs = 5
static let NumberOfDivsFinishedLoadingAttempts = 10
// A page doesn't have enough time to load on old devices
static let NumberOfDivsLoadingDelay = 0.5
} }
#if DEBUG #if DEBUG
@ -44,9 +119,6 @@ internal class ReCaptchaWebViewManager {
public var shouldSkipForTests = false public var shouldSkipForTests = false
#endif #endif
/// Callback for loading state changing
var onLoadingChanged: ReCaptcha.BoolParameterClosure?
/// Sends the result message /// Sends the result message
var completion: ((ReCaptchaResult) -> Void)? var completion: ((ReCaptchaResult) -> Void)?
@ -63,39 +135,29 @@ internal class ReCaptchaWebViewManager {
fileprivate var decoder: ReCaptchaDecoder! fileprivate var decoder: ReCaptchaDecoder!
/// Indicates if the script has already been loaded by the `webView` /// Indicates if the script has already been loaded by the `webView`
fileprivate var didFinishLoading = false { // webView.isLoading does not work for WKWebview.loadHTMLString fileprivate var didFinishLoading = false // webView.isLoading does not work in this case
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` /// The observer for `.UIWindowDidBecomeVisible`
fileprivate var observer: NSObjectProtocol? fileprivate var observer: NSObjectProtocol?
/// The observer for `\WKWebView.estimatedProgress`
fileprivate var loadingObservation: NSKeyValueObservation?
/// The endpoint url being used /// The endpoint url being used
fileprivate var endpoint: String fileprivate var endpoint: String
/// The `webView` delegate implementation
fileprivate lazy var webviewDelegate: WebViewDelegate = {
WebViewDelegate(manager: self)
}()
/// The webview that executes JS code /// The webview that executes JS code
lazy var webView: WKWebView = { lazy var webView: WKWebView = {
let webview = WKWebView( let webview = WKWebView(
frame: CGRect(x: 0, y: 0, width: 1, height: 1), frame: CGRect(x: 0, y: 0, width: 1, height: 1),
configuration: self.buildConfiguration() configuration: self.buildConfiguration()
) )
webview.navigationDelegate = self.webviewDelegate
webview.accessibilityIdentifier = "webview" webview.accessibilityIdentifier = "webview"
webview.accessibilityTraits = UIAccessibilityTraits.link webview.accessibilityTraits = UIAccessibilityTraits.link
webview.isHidden = true webview.isHidden = true
self.loadingObservation = webview.observe(\.estimatedProgress, options: [.new]) { [weak self] _, change in
self?.didFinishLoading = change.newValue == 1
}
return webview return webview
}() }()
@ -109,7 +171,6 @@ internal class ReCaptchaWebViewManager {
*/ */
init(html: String, apiKey: String, baseURL: URL, endpoint: String) { init(html: String, apiKey: String, baseURL: URL, endpoint: String) {
self.endpoint = endpoint self.endpoint = endpoint
self.decoder = ReCaptchaDecoder { [weak self] result in self.decoder = ReCaptchaDecoder { [weak self] result in
self?.handle(result: result) self?.handle(result: result)
} }
@ -137,7 +198,6 @@ internal class ReCaptchaWebViewManager {
Starts the challenge validation Starts the challenge validation
*/ */
func validate(on view: UIView) { func validate(on view: UIView) {
onLoadingChanged?(true)
#if DEBUG #if DEBUG
guard !shouldSkipForTests else { guard !shouldSkipForTests else {
completion?(.token("")) completion?(.token(""))
@ -164,6 +224,7 @@ internal class ReCaptchaWebViewManager {
func reset() { func reset() {
didFinishLoading = false didFinishLoading = false
configureWebViewDispatchToken = UUID() configureWebViewDispatchToken = UUID()
webviewDelegate.reset()
webView.evaluateJavaScript(Constants.ResetCommand) { [weak self] _, error in webView.evaluateJavaScript(Constants.ResetCommand) { [weak self] _, error in
if let error = error { if let error = error {
@ -178,7 +239,7 @@ internal class ReCaptchaWebViewManager {
/** Private methods for ReCaptchaWebViewManager /** Private methods for ReCaptchaWebViewManager
*/ */
fileprivate extension ReCaptchaWebViewManager { fileprivate extension ReCaptchaWebViewManager {
/** Executes the JS command that loads the ReCaptcha challenge after a page finished loading. /** Executes the JS command that loads the ReCaptcha challenge.
This method has no effect if the webview hasn't finished loading. This method has no effect if the webview hasn't finished loading.
*/ */
func execute() { func execute() {
@ -187,65 +248,10 @@ fileprivate extension ReCaptchaWebViewManager {
return return
} }
evaluateExecuteWhenLoadingFinished(count: 0)
}
/**
- parameter count: Number of checks of number of divs
Executes the JS command that returns number of divs.
*/
func evaluateExecuteWhenLoadingFinished(count: Int) {
webView.evaluateJavaScript(Constants.NumberOfDivsCommand) { [weak self] (result, error) -> Void in
if let error = error {
self?.decoder.send(error: .unexpected(error))
self?.onLoadingChanged?(false)
} else {
self?.handleNumberOfDivs(result: result, count: count)
}
}
}
/**
- parameters:
- result: Result of number of divs command evaluation
- count: Number of checks of divs count
Handles number of divs command result.
*/
func handleNumberOfDivs(result: Any?, count: Int) {
if let result = result as? Int, result >= Constants.MinNumberOfDivs {
evaluateExecute()
} else {
handleInvalidNumberOfDivsResult(count: count)
}
}
/**
- parameter count: Number of checks of number of divs
Handles invalid number of divs.
*/
func handleInvalidNumberOfDivsResult(count: Int) {
if count < Constants.NumberOfDivsFinishedLoadingAttempts {
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.NumberOfDivsLoadingDelay) { [weak self] in
self?.evaluateExecuteWhenLoadingFinished(count: count + 1)
}
} else {
decoder.send(error: .htmlLoadError)
onLoadingChanged?(false)
}
}
/**
Executes the JS command that loads the ReCaptcha challenge.
*/
func evaluateExecute() {
webView.evaluateJavaScript(Constants.ExecuteJSCommand) { [weak self] _, error in webView.evaluateJavaScript(Constants.ExecuteJSCommand) { [weak self] _, error in
if let error = error { if let error = error {
self?.decoder.send(error: .unexpected(error)) self?.decoder.send(error: .unexpected(error))
} }
self?.onLoadingChanged?(false)
} }
} }
@ -291,7 +297,7 @@ fileprivate extension ReCaptchaWebViewManager {
case .didLoad: case .didLoad:
// For testing purposes // For testing purposes
didFinishLoading = true webviewDelegate.execute()
case .log(let message): case .log(let message):
#if DEBUG #if DEBUG

View File

@ -9,10 +9,6 @@
import RxSwift import RxSwift
import UIKit import UIKit
public enum ReCaptchaRxError: Error {
case baseWasReleased
}
/// Makes ReCaptcha compatible with RxSwift extensions /// Makes ReCaptcha compatible with RxSwift extensions
extension ReCaptcha: ReactiveCompatible {} extension ReCaptcha: ReactiveCompatible {}
@ -68,20 +64,4 @@ public extension Reactive where Base: ReCaptcha {
base?.reset() base?.reset()
} }
} }
/**
Observable of loading state
(will not work if someone changes onLoadingChanged variable; current onLodinglChanged will not work after subscription)
*/
var loadingObservable: Observable<Bool> {
return .create { [weak base] observer -> Disposable in
guard let base = base else {
observer.onError(ReCaptchaRxError.baseWasReleased)
return Disposables.create()
}
base.onLoadingChanged = { observer.onNext($0) }
return Disposables.create { [weak base] in base?.onLoadingChanged = nil }
}
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB