How to add a JavaScript function in WebView and ca

2019-04-05 15:22发布

问题:

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?

回答1:

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));


回答2:

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.



回答3:

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>