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?
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));
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 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);
/* also add your additional JavaScript functions
and additional code in this function */
}
})()
""".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) {
android.reCaptchaCallbackInAndroid(token);
/* also you can add further JavaScript functions
and additional code in this function */
}
})()
""".trimIndent())
Test:
Here's a simple, Empty Activity
in Android Studio with a basic LinearLayout
( an EditText
and a Button
within the layout) and the MainActivity.kt
:
package com.richardszkcs.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://richardszkcs.github.io/recaptcha-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 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:
Full disclosure: I made the reCAPTCHA test website to prepare/test/debug similar situations.
IN JAVA
MyApplication.java
public class MyApplication extends Application {
public static final String TAG = MyApplication.class
.getSimpleName();
private RequestQueue mRequestQueue;
private static MyApplication mInstance;
@Override
public void onCreate() {
super.onCreate();
mInstance = this;
}
public static synchronized MyApplication getInstance() {
return mInstance;
}
public RequestQueue getRequestQueue() {
if (mRequestQueue == null) {
mRequestQueue = Volley.newRequestQueue(getApplicationContext());
}
return mRequestQueue;
}
public <T> void addToRequestQueue(Request<T> req, String tag) {
// set the default tag if tag is empty
req.setTag(TextUtils.isEmpty(tag) ? TAG : tag);
getRequestQueue().add(req);
}
public <T> void addToRequestQueue(Request<T> req) {
req.setTag(TAG);
getRequestQueue().add(req);
}
public void cancelPendingRequests(Object tag) {
if (mRequestQueue != null) {
mRequestQueue.cancelAll(tag);
}
}
}
**MainActivity.java**
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
// TODO - replace the SITE KEY with yours
private static final String SAFETY_NET_API_SITE_KEY = " YOUR SITE KEY HEAR";
// TODO - replace the SERVER URL with yours
private static final String URL_VERIFY_ON_SERVER = "https://example.com/index.php";
@BindView(R.id.input_feedback)
EditText inputFeedback;
@BindView(R.id.layout_feedback_form)
LinearLayout layoutFeedbackForm;
@BindView(R.id.message_feedback_done)
TextView messageFeedbackDone;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setTitle(getString(R.string.feedback));
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Toast.makeText(getApplicationContext(), "Always check Android Studio `LogCat` for errors!", Toast.LENGTH_LONG).show();
}
@OnClick(R.id.btn_send)
public void validateCaptcha() {
String feedback = inputFeedback.getText().toString().trim();
// checking for empty feedback message
if (TextUtils.isEmpty(feedback)) {
Toast.makeText(getApplicationContext(), "Enter feedback!", Toast.LENGTH_LONG).show();
return;
}
// Showing reCAPTCHA dialog
SafetyNet.getClient(this).verifyWithRecaptcha(SAFETY_NET_API_SITE_KEY)
.addOnSuccessListener(this, new OnSuccessListener<SafetyNetApi.RecaptchaTokenResponse>() {
@Override
public void onSuccess(SafetyNetApi.RecaptchaTokenResponse response) {
Log.d(TAG, "onSuccess");
if (!response.getTokenResult().isEmpty()) {
// Received captcha token
// This token still needs to be validated on the server
// using the SECRET key
verifyTokenOnServer(response.getTokenResult());
}
}
})
.addOnFailureListener(this, new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
if (e instanceof ApiException) {
ApiException apiException = (ApiException) e;
Log.d(TAG, "Error message: " +
CommonStatusCodes.getStatusCodeString(apiException.getStatusCode()));
} else {
Log.d(TAG, "Unknown type of error: " + e.getMessage());
}
}
});
}
/**
* Verifying the captcha token on the server
* Post param: recaptcha-response
* Server makes call to https://www.google.com/recaptcha/api/siteverify
* with SECRET Key and Captcha token
*/
public void verifyTokenOnServer(final String token) {
Log.d(TAG, "Captcha Token" + token);
StringRequest strReq = new StringRequest(Request.Method.POST,
URL_VERIFY_ON_SERVER, new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.d(TAG, response.toString());
try {
JSONObject jsonObject = new JSONObject(response);
boolean success = jsonObject.getBoolean("success");
String message = jsonObject.getString("message");
if (success) {
// Congrats! captcha verified successfully on server
// TODO - submit the feedback to your server
layoutFeedbackForm.setVisibility(View.GONE);
messageFeedbackDone.setVisibility(View.VISIBLE);
} else {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
}
} catch (JSONException e) {
e.printStackTrace();
Toast.makeText(getApplicationContext(), "Json Error: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e(TAG, "Error: " + error.getMessage());
}
}) {
@Override
protected Map<String, String> getParams() {
Map<String, String> params = new HashMap<>();
params.put("recaptcha-response", token);
return params;
}
};
MyApplication.getInstance().addToRequestQueue(strReq);
}
}
build.gradle
// SafetyNet reCAPTCHA
implementation 'com.google.android.gms:play-services-safetynet:11.8.0'
// ButterKnife
implementation 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_main" />
</android.support.design.widget.CoordinatorLayout>
content_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/activity_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="info.androidhive.recaptcha.MainActivity"
tools:showIn="@layout/activity_main">
<LinearLayout
android:id="@+id/layout_feedback_form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/title_form"
android:textColor="#666666"
android:textSize="20dp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/desc_form" />
<EditText
android:id="@+id/input_feedback"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:gravity="top"
android:hint="@string/hint_feedback"
android:lines="5" />
<Button
android:id="@+id/btn_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
style="@style/Widget.AppCompat.Button.Colored"
android:text="@string/btn_send"
android:textColor="@android:color/white" />
</LinearLayout>
<TextView
android:id="@+id/message_feedback_done"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:gravity="center"
android:padding="@dimen/activity_margin"
android:text="@string/message_feedback_done"
android:textSize="22dp"
android:visibility="gone" />
</LinearLayout>