Compare commits

...

15 Commits

Author SHA1 Message Date
Artur b40f0ac185 Update to master branch 2019-10-03 16:09:07 +03:00
Artur Azarau dbcad92ff0 cartfile resolved updated 2019-10-02 09:51:39 +03:00
Artur Azarau 605e1aeeef branch changed 2019-10-02 09:49:50 +03:00
Ivan Babkin 6e712bf664 Carthage update 2019-06-17 19:43:13 +03:00
Ivan Babkin 0b860372e9
Merge pull request #3 from ZharaOo/fix/cartfile
Using RxSwift binary
2019-06-17 19:39:04 +03:00
Ivan Babkin c8f4fb213e Using RxSwift binary 2019-06-17 19:37:56 +03:00
Ivan Babkin 7ba0aff437
Merge pull request #2 from ZharaOo/feature/loading_observable
Added loadingObservable property
2019-06-17 19:32:57 +03:00
Ivan Babkin b3994f932e Added possibility to observe loading state changing 2019-06-17 19:32:23 +03:00
Ivan Babkin 1d178f7154
Merge pull request #1 from ZharaOo/fix/execute
Fixed unexpected error on old devices
2019-06-11 14:16:28 +03:00
Ivan Babkin 784187e911 Fixed unexpected error on old devices 2019-06-03 22:44:24 +03:00
Flávio Caetano 31a64e5967 Update issue template to include ReCaptcha v2 docs 2019-04-04 17:21:28 -03:00
Flávio Caetano ce8acad6a2 Add incompatibility with ReCaptcha v3 to README 2019-04-04 17:17:42 -03:00
Rachit Mishra 0e47c7264f Add warning to uncheck verify origin settings in recaptcha (#68)
* Add warning to uncheck verify origin 

If verify origin is checked it leads to JS error code 4

* Add screenshot for verify domain settings

* Update file name for verify origin settings

* Add example image for verify origin

* Remove redundant space
2019-03-20 10:59:44 -03:00
Flávio Caetano c9afbc22c1 Version Bump (1.4.2) 2018-12-06 16:02:45 -02:00
Flávio Caetano 050ce19425 Fix webview's resource loading detection
fix #56
2018-12-06 15:53:51 -02:00
12 changed files with 137 additions and 109 deletions

View File

@ -1,14 +1,13 @@
---
name: Bug report
about: Create a report to help us improve
---
<!--
## Is it really a bug?
Before opening an issue, check the following:
1. You are using the **Client side integration** key
1. You are using the **SITE** key
2. The correct domain, with protocol, is setup.
3. You are using an **Invisible** reCAPTCHA key.
3. You are using an **Invisible** reCAPTCHA v2 key.
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
@ -20,7 +19,7 @@ A clear and concise description of what the bug is.
## To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
2. Click on '...'
3. ...
4. Profit (jk See error)

View File

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

View File

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

View File

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

View File

@ -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")
}

View File

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

View File

@ -9,7 +9,7 @@
-----
Add Google's [Invisible ReCaptcha](https://developers.google.com/recaptcha/docs/invisible) to your project. This library
Add Google's [Invisible ReCaptcha v2](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
invisibility is not possible.
@ -17,8 +17,15 @@ invisibility is not possible.
#### _Warning_ ⚠️
Beware that this library only works for Invisible ReCaptcha keys! Make sure to check the Invisible reCAPTCHA option
when creating your [API Key](https://www.google.com/recaptcha/admin).
Beware that this library only works for ReCaptcha v2 Invisible keys! Make sure to check the reCAPTCHA
v2 Invisible badge option when creating your [API Key](https://www.google.com/recaptcha/admin/create).
![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
@ -42,7 +49,7 @@ extension for the ReCaptcha framework.
## Usage
Simply add `ReCaptchaKey` and `ReCaptchaDomain` (with a protocol) to your Info.plist and run:
Simply add `ReCaptchaKey` and `ReCaptchaDomain` (with a protocol ex. http:// or https://) to your Info.plist and run:
``` swift
let recaptcha = try? ReCaptcha()

View File

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

View File

@ -13,6 +13,8 @@ import WebKit
/**
*/
public class ReCaptcha {
public typealias BoolParameterClosure = (Bool) -> ()
fileprivate struct Constants {
struct InfoDictKeys {
static let APIKey = "ReCaptchaKey"
@ -101,6 +103,12 @@ 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
let manager: ReCaptchaWebViewManager

View File

@ -13,93 +13,18 @@ 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<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 {
static let ExecuteJSCommand = "execute();"
static let ResetCommand = "reset();"
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
@ -119,6 +44,9 @@ internal class ReCaptchaWebViewManager {
public var shouldSkipForTests = false
#endif
/// Callback for loading state changing
var onLoadingChanged: ReCaptcha.BoolParameterClosure?
/// Sends the result message
var completion: ((ReCaptchaResult) -> Void)?
@ -135,29 +63,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
}()
@ -171,6 +109,7 @@ internal class ReCaptchaWebViewManager {
*/
init(html: String, apiKey: String, baseURL: URL, endpoint: String) {
self.endpoint = endpoint
self.decoder = ReCaptchaDecoder { [weak self] result in
self?.handle(result: result)
}
@ -198,6 +137,7 @@ internal class ReCaptchaWebViewManager {
Starts the challenge validation
*/
func validate(on view: UIView) {
onLoadingChanged?(true)
#if DEBUG
guard !shouldSkipForTests else {
completion?(.token(""))
@ -224,7 +164,6 @@ internal class ReCaptchaWebViewManager {
func reset() {
didFinishLoading = false
configureWebViewDispatchToken = UUID()
webviewDelegate.reset()
webView.evaluateJavaScript(Constants.ResetCommand) { [weak self] _, error in
if let error = error {
@ -239,7 +178,7 @@ internal class ReCaptchaWebViewManager {
/** Private methods for ReCaptchaWebViewManager
*/
fileprivate extension ReCaptchaWebViewManager {
/** Executes the JS command that loads the ReCaptcha challenge.
/** Executes the JS command that loads the ReCaptcha challenge after a page finished loading.
This method has no effect if the webview hasn't finished loading.
*/
func execute() {
@ -248,10 +187,65 @@ fileprivate extension ReCaptchaWebViewManager {
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
if let error = error {
self?.decoder.send(error: .unexpected(error))
}
self?.onLoadingChanged?(false)
}
}
@ -297,7 +291,7 @@ fileprivate extension ReCaptchaWebViewManager {
case .didLoad:
// For testing purposes
webviewDelegate.execute()
didFinishLoading = true
case .log(let message):
#if DEBUG

View File

@ -9,6 +9,10 @@
import RxSwift
import UIKit
public enum ReCaptchaRxError: Error {
case baseWasReleased
}
/// Makes ReCaptcha compatible with RxSwift extensions
extension ReCaptcha: ReactiveCompatible {}
@ -64,4 +68,20 @@ public extension Reactive where Base: ReCaptcha {
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 }
}
}
}

BIN
example-v2-key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB