Add verification for ReCaptcha config

This will verify if ReCaptchaKey and ReCaptchaDomain are correctly configured and the JS lib was able to load correctly
This commit is contained in:
Flávio Caetano 2019-04-10 19:26:53 -03:00
parent 31a64e5967
commit 4e6c022a23
7 changed files with 119 additions and 55 deletions

View File

@ -58,9 +58,11 @@ class ViewController: UIViewController {
}
@IBAction private func didPressButton(button: UIButton) {
label.text = ""
disposeBag = DisposeBag()
let validate = recaptcha.rx.validate(on: view)
let validate = recaptcha.rx.validate(on: view, resetOnError: false)
.debug("validate")
.share()

View File

@ -164,4 +164,25 @@ class ReCaptchaDecoder__Tests: XCTestCase {
// Check
XCTAssertEqual(result, .didLoad)
}
func test__Decode__Verify() {
let exp = expectation(description: "send verify")
let expectedResult = true
var result: Result?
assertResult = { res in
result = res
exp.fulfill()
}
// Send
let message = MockMessage(message: ["verify": expectedResult])
decoder.send(message: message)
waitForExpectations(timeout: 1)
// Check
XCTAssertEqual(result, .verify(expectedResult))
}
}

View File

@ -48,6 +48,9 @@ extension ReCaptchaDecoder.Result: Equatable {
case (.error(let lhe), .error(let rhe)):
return lhe == rhe
case (.verify(let lhv), .verify(let rhv)):
return lhv == rhv
default:
return false
}

View File

@ -2,8 +2,8 @@
<head>
<meta name="viewport" content="width=device-width" />
<script type="text/javascript">
var key = "${apiKey}";
var endpoint = "${endpoint}";
var key = '${apiKey}';
var endpoint = '${endpoint}';
var shouldFail = ${shouldFail};
var post = function(value) {
@ -12,7 +12,7 @@
var execute = function() {
if (shouldFail) {
post("error");
post('error');
}
else {
post(${message});
@ -21,7 +21,11 @@
var reset = function() {
shouldFail = false;
post({action: "didLoad"});
post({ action: 'didLoad' });
};
var verify = function() {
post({ verify: shouldFail });
};
</script>
</head>

View File

@ -7,12 +7,12 @@
};
console.log = function(message) {
post({log: message});
post({ log: message });
};
var showReCaptcha = function() {
console.log("showReCaptcha");
post({action: "showReCaptcha"});
console.log('showReCaptcha');
post({ action: 'showReCaptcha' });
};
var observeDOM = function(element, completion) {
@ -21,42 +21,49 @@
completion();
});
})
.observe(element, {attributes: true, attributeFilter: ['style']})
.observe(element, {
attributes: true,
attributeFilter: ['style'],
});
};
var execute = function() {
console.log("executing");
console.log('executing');
// Removes ReCaptcha dismissal when clicking outside div area
try {
document.getElementsByTagName("div")[4].outerHTML = ""
}
catch(e) {
document.getElementsByTagName('div')[4].outerHTML = '';
}
catch(e) {}
// Listens to changes on the div element that presents the ReCaptcha challenge
observeDOM(document.getElementsByTagName("div")[3], showReCaptcha);
observeDOM(document.getElementsByTagName('div')[3], showReCaptcha);
grecaptcha.execute();
};
var onSubmit = function(token) {
console.log(token);
post({token: token});
post({ token });
};
var onloadCallback = function() {
console.log("did load");
console.log('did load');
grecaptcha.render('submit', {
'sitekey' : '${apiKey}',
'callback' : onSubmit,
'size': 'invisible'
sitekey : '${apiKey}',
callback : onSubmit,
size: 'invisible'
});
};
var reset = function() {
console.log("resetting");
grecaptcha.reset();
console.log('resetting');
grecaptcha.reset();
};
var verify = function() {
const challengeDiv = document.getElementsByTagName('div')[3];
post({ verify: challengeDiv !== undefined });
};
</script>
</head>

View File

@ -30,6 +30,9 @@ internal class ReCaptchaDecoder: NSObject {
/// Logs a string onto the console
case log(String)
/// Verifies the configuration
case verify(Bool)
}
/// The closure that receives messages
@ -89,6 +92,12 @@ fileprivate extension ReCaptchaDecoder.Result {
if let token = response["token"] as? String {
return .token(token)
}
else if let success = response["verify"] as? Bool {
return .verify(success)
}
else if let message = response["log"] as? String {
return .log(message)
}
if let action = response["action"] as? String {
switch action {
@ -103,10 +112,6 @@ fileprivate extension ReCaptchaDecoder.Result {
}
}
if let message = response["log"] as? String {
return .log(message)
}
return .error(.wrongMessageFormat)
}
}

View File

@ -13,10 +13,13 @@ import WebKit
/** Handles comunications with the webview containing the ReCaptcha challenge.
*/
internal class ReCaptchaWebViewManager {
enum JSCommand: String {
case execute = "execute();"
case reset = "reset();"
case verify = "verify();"
}
fileprivate struct Constants {
static let ExecuteJSCommand = "execute();"
static let ResetCommand = "reset();"
static let BotUserAgent = "Googlebot/2.1"
}
@ -55,11 +58,15 @@ internal class ReCaptchaWebViewManager {
/// Indicates if the script has already been loaded by the `webView`
fileprivate var didFinishLoading = false { // webView.isLoading does not work for WKWebview.loadHTMLString
didSet {
if didFinishLoading && completion != nil {
if didFinishLoading {
// 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()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.executeJS(command: .verify)
if self?.completion != nil {
self?.executeJS(command: .execute)
}
}
}
}
@ -135,7 +142,7 @@ internal class ReCaptchaWebViewManager {
webView.isHidden = false
view.addSubview(webView)
execute()
executeJS(command: .execute)
}
@ -150,14 +157,9 @@ internal class ReCaptchaWebViewManager {
The reset is achieved by calling `grecaptcha.reset()` on the JS API.
*/
func reset() {
didFinishLoading = false
configureWebViewDispatchToken = UUID()
webView.evaluateJavaScript(Constants.ResetCommand) { [weak self] _, error in
if let error = error {
self?.decoder.send(error: .unexpected(error))
}
}
executeJS(command: .reset)
didFinishLoading = false
}
}
@ -166,22 +168,6 @@ internal class ReCaptchaWebViewManager {
/** Private methods for ReCaptchaWebViewManager
*/
fileprivate extension ReCaptchaWebViewManager {
/** Executes the JS command that loads the ReCaptcha challenge.
This method has no effect if the webview hasn't finished loading.
*/
func execute() {
guard didFinishLoading else {
// Hasn't finished loading the HTML yet
return
}
webView.evaluateJavaScript(Constants.ExecuteJSCommand) { [weak self] _, error in
if let error = error {
self?.decoder.send(error: .unexpected(error))
}
}
}
/**
- returns: An instance of `WKWebViewConfiguration`
@ -208,6 +194,10 @@ fileprivate extension ReCaptchaWebViewManager {
completion?(.token(token))
case .error(let error):
#if DEBUG
print("[JS ERROR]:", error)
#endif
if shouldResetOnError, let view = webView.superview {
reset()
validate(on: view)
@ -228,8 +218,20 @@ fileprivate extension ReCaptchaWebViewManager {
case .log(let message):
#if DEBUG
print("[JS LOG]:", message)
print("[JS LOG]:", message)
#endif
case .verify(let success):
if !success {
#if DEBUG
// swiftlint:disable line_length
print("""
WARNING! ReCaptcha wasn't successfully configured. Please double check your ReCaptchaKey and ReCaptchaDomain.
Also check that you're using ReCaptcha's **SITE KEY** for client side integration.
""")
// swiftlint:enable line_length
#endif
}
}
}
@ -249,4 +251,24 @@ fileprivate extension ReCaptchaWebViewManager {
NotificationCenter.default.removeObserver(observer)
}
}
/**
- parameters:
- command: The JS command to be executed
Executes the given JS command. If an error happens, it is thrown to the user. This method has no effect if the
webview hasn't finished loading.
*/
func executeJS(command: JSCommand) {
guard didFinishLoading else {
// Hasn't finished loading the HTML yet
return
}
webView.evaluateJavaScript(command.rawValue) { [weak self] _, error in
if let error = error {
self?.decoder.send(error: .unexpected(error))
}
}
}
}