12

I am adding a JavaScript function in WebView like this (Kotlin):

val webView = findViewById(R.id.webview) as WebView
webView.getSettings().setJavaScriptEnabled(true)
webView.addJavascriptInterface(this, "android")
webView.getSettings().setBuiltInZoomControls(false)
webView.loadUrl(url)

webView.webViewClient = object : WebViewClient() {
    override fun onPageFinished(view: WebView, url: String) {
        super.onPageFinished(view, url)
        webView.loadUrl("javascript:(function captchaResponse (token){" +
                        "      android.reCaptchaCallbackInAndroid(token);" +
                        "    })()")
    }
}

The function works fine, but the problem is that it runs immediately, when I add it in WebView. I only want to include it as a JavaScript function and it should be called only from the HTML, when the user will fill the reCAPTCHA. How can I do that?

Rick
  • 4,030
  • 9
  • 24
  • 35
Zohab Ali
  • 8,426
  • 4
  • 55
  • 63

2 Answers2

10

In order to run your reCaptchaCallbackInAndroid exposed method from JavaScript, when the user submitted a successful reCAPTCHA response, first make sure, to actually listen to the reCAPTCHA callback via g-recaptcha tag attributes:

<div class="g-recaptcha"
     data-sitekey="{{your site key}}"
     data-callback="myCustomJavaScriptCallback"
></div>

or via the reCAPTCHA v2 JavaScript API:

grecaptcha.render(
  'g-recaptcha-element-id', {
    sitekey: '{{your site key}}',
    callback: 'myCustomJavaScriptCallback'
  }
)

then, when the page finished loading in the WebView, add your JavaScript callback function to the window object using webView.loadUrl:

webView.loadUrl("""
    javascript:(function() {
        window.myCustomJavaScriptCallback = function(token) {                
            android.reCaptchaCallbackInAndroid(token);
        }
    })()
""".trimIndent())

and finally, when the user submits a successful reCAPTCHA response, your myCustomJavaScriptCallback will be called and through that, your exposed reCaptchaCallbackInAndroid method too with the reCAPTCHA token.

  • Since you're using Kotlin, in this case, you can just simply use multiline string literals.

  • Since you're exposing a method to JavaScript, make sure to know the security concerns.

  • In case you'll need additional JavaScript injection in the future (more method exposure, DOM manipulation, etc.), check out this post.


In your case:

Set reCAPTCHA to call your captchaResponse JavaScript function via tag attribute:

<div class="g-recaptcha"
     ...
     data-callback="captchaResponse"
     ...
></div>

or via its API:

grecaptcha.render(
  '...', {
    ...
    callback: 'captchaResponse'
    ...
  }
)

and add your captchaResponse callback function to window:

webView.loadUrl("""
    javascript:(function() {
        window.captchaResponse = function(token) {
            // your code here before the Android callback...

            android.reCaptchaCallbackInAndroid(token);
            
            // ...or after the Android callback
        }
    })()
""".trimIndent())

Test:

Here's a simple, Empty Activity in Android Studio (using Kotlin) with a basic LinearLayout (an EditText and a Button within the layout) and the MainActivity.kt:

package com.richrdkng.injectjsintowebview

import android.net.Uri
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.webkit.JavascriptInterface
import kotlinx.android.synthetic.main.activity_main.*
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        sendButton.setOnClickListener { loadWebpage() }
    }

    @Throws(UnsupportedOperationException::class)
    fun buildUri(authority: String) : Uri {
        val builder = Uri.Builder()
        builder.scheme("https")
                .authority(authority)
        return builder.build()
    }

    @JavascriptInterface
    fun reCaptchaCallbackInAndroid(token: String) {
        val tok = token.substring(0, token.length / 2) + "..."
        Toast.makeText(this.applicationContext, tok, Toast.LENGTH_LONG).show()
    }

    fun loadWebpage() {
        webView.getSettings().setJavaScriptEnabled(true)
        webView.addJavascriptInterface(this, "android")
        webView.getSettings().setBuiltInZoomControls(false)
        webView.loadUrl("https://www.richrdkng.com/recaptcha-v2-test/")

        webView.webViewClient = object : WebViewClient() {
            override fun onPageFinished(view: WebView, url: String) {
                super.onPageFinished(view, url)
                webView.loadUrl("""
                    javascript:(function() {
                        window.onCaptchaSuccess = function(token) {
                            android.reCaptchaCallbackInAndroid(token);
                        }
                    })()
                """.trimIndent())
            }
        }
    }
}

then using a simple reCAPTCHA v2 test website, the window.onCaptchaSuccess function is called upon a successful reCAPTCHA submission and the reCAPTCHA token is partially displayed in a Toast using an Android Emulator:

virtual machine with successful reCAPTCHA v2


Full disclosure: I made the reCAPTCHA v2 test website to prepare/test/debug similar situations.

Rick
  • 4,030
  • 9
  • 24
  • 35
  • I cannot just use webView.loadUrl to push javascript code. Problem is the javascript code that I am pushing contains script which hides the recaptcha. So when I add it with webView.loadUrl it runs immediately and hides recaptcha. I am already doing exactly same thing which you have described. All I want is that javascript code should not be executed IMMEDIATELY when I add it to webview – Zohab Ali Aug 01 '18 at 05:49
  • @ZohabAli, can you please update your question and post the **full code** you want to run including **hiding the reCAPTCHA with all the additional details you want/need to do**, so I can update my answer accordingly? Also, please post the HTML code too, so we can help you better. Thank you in advance. – Rick Aug 03 '18 at 10:52
  • 1
    Your solution helped me and my team to solve a not so simple problem. Can you please check the recaptcha link in your answer? Thx! – user7637745 Feb 02 '22 at 21:03
  • 1
    Fixed and updated. You're welcome! – Rick Feb 03 '22 at 18:32
0

Try injecting the script like this,

function addCode(code){
var addedScript= document.createElement('script');
addedScript.text= code;
document.body.appendChild(addedScript);}

now call the function like,

val codeToExec = "function captchaResponse (token){" +
                    "android.reCaptchaCallbackInAndroid(token);" +
                    "}";

now exec loadurl like,

webview.loadUrl("javascript:(function addCode(code){
var addedScript= document.createElement('script');
addedScript.text= code;
document.body.appendChild(addedScript);})(codeToExec));
Saikrishna Rajaraman
  • 3,205
  • 2
  • 16
  • 29