Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there any recaptcha v2 close event?

I am rendering grecaptcha with code like this

let callback;
const p = new Promise((resolve) => callback = (result) => resolve(result));

grecaptcha.render(el, {
    sitekey: window.settings.recaptchaKey,
    size: "invisible",
    type: "image",
    callback: result => callback(result),
    badge: "inline"
});

const key = await p;

all working fine, but if user clicks on the backdrop of recaptcha modal, recaptcha closes and i can't detect it, so i wait infinite for response

i need some kind of event or callback to detect when it closes

like image 745
ForceUser Avatar asked Oct 29 '22 06:10

ForceUser


2 Answers

Unfortunately, Google doesn't have an API event to track this, but we can use the Mutation Observer Web API to track DOM changes by Google API on our own.

We have 2 challenges here.

1) Detect when the challenge is shown and get the overlay div of the challenge

function detectWhenReCaptchaChallengeIsShown() {
    return new Promise(function(resolve) {
        const targetElement = document.body;

        const observerConfig = {
            childList: true,
            attributes: false,
            attributeOldValue: false,
            characterData: false,
            characterDataOldValue: false,
            subtree: false
        };

        function DOMChangeCallbackFunction(mutationRecords) {
            mutationRecords.forEach((mutationRecord) => {
                if (mutationRecord.addedNodes.length) {
                    var reCaptchaParentContainer = mutationRecord.addedNodes[0];
                    var reCaptchaIframe = reCaptchaParentContainer.querySelectorAll('iframe[title*="recaptcha"]');

                    if (reCaptchaIframe.length) {
                        var reCaptchaChallengeOverlayDiv = reCaptchaParentContainer.firstChild;
                        if (reCaptchaChallengeOverlayDiv.length) {
                            reCaptchaObserver.disconnect();
                            resolve(reCaptchaChallengeOverlayDiv);
                        }
                    }
                }
            });
        }

        const reCaptchaObserver = new MutationObserver(DOMChangeCallbackFunction);
        reCaptchaObserver.observe(targetElement, observerConfig);
    });
}

First, we created a target element that we would observe for Google iframe appearance. We targeted document.body as an iframe will be appended to it:

const targetElement = document.body;

Then we created a config object for MutationObserver. Here we might specify what exactly we track in DOM changes. Please note that all values are 'false' by default so we could only leave 'childList' - which means that we would observe only the child node changes for the target element - document.body in our case:

const observerConfig = {
    childList: true,
    attributes: false,
    attributeOldValue: false,
    characterData: false,
    characterDataOldValue: false,
    subtree: false
};

Then we created a function that would be invoked when an observer detects a specific type of DOM change that we specified in config object. The first argument represents an array of Mutation Observer objects. We grabbed the overlay div and returned in with Promise.

function DOMChangeCallbackFunction(mutationRecords) {
    mutationRecords.forEach((mutationRecord) => {
        if (mutationRecord.addedNodes.length) { //check only when notes were added to DOM
            var reCaptchaParentContainer = mutationRecord.addedNodes[0];
            var reCaptchaIframe = reCaptchaParentContainer.querySelectorAll('iframe[title*="recaptcha"]');

            if (reCaptchaIframe.length) { // Google reCaptcha iframe was loaded
                var reCaptchaChallengeOverlayDiv = reCaptchaParentContainer.firstChild;
                if (reCaptchaChallengeOverlayDiv.length) {
                    reCaptchaObserver.disconnect(); // We don't want to observe more DOM changes for better performance
                    resolve(reCaptchaChallengeOverlayDiv); // Returning the overlay div to detect close events
                }
            }
        }
    });
}

Lastly we instantiated an observer itself and started observing DOM changes:

const reCaptchaObserver = new MutationObserver(DOMChangeCallbackFunction);
reCaptchaObserver.observe(targetElement, observerConfig);

2) Second challenge is the main question of that post - how do we detect that the challenge is closed? Well, we need help of MutationObserver again.

detectReCaptchaChallengeAppearance().then(function (reCaptchaChallengeOverlayDiv) {
    var reCaptchaChallengeClosureObserver = new MutationObserver(function () {
        if ((reCaptchaChallengeOverlayDiv.style.visibility === 'hidden') && !grecaptcha.getResponse()) {
            // TADA!! Do something here as the challenge was either closed by hitting outside of an overlay div OR by pressing ESC key
            reCaptchaChallengeClosureObserver.disconnect();
        }
    });
    reCaptchaChallengeClosureObserver.observe(reCaptchaChallengeOverlayDiv, {
        attributes: true,
        attributeFilter: ['style']
    });
});

So what we did is we get the Google reCaptcha challenge overlay div with the Promise we created in Step1 and then we subscribed for "style" changes on overlay div. This is because when the challenge is closed - Google fade it out. It's important to note that the visibility will be also hidden when a person solves the captcha successfully. That is why we added !grecaptcha.getResponse() check. It will return nothing unless the challenge is resolved. This is pretty much it - I hope that helps :)

like image 114
kosmeln Avatar answered Nov 09 '22 11:11

kosmeln


As a dirty workaround, we can set timeout and wait for recaptcha iframe to show and then wait for it to hide

I made module that makes all manipulations

It depends on jquery and global recaptcha

and i use it like this

try {
    key = await captcha(elementToBind, 'yoursitekey');
}
catch (error) {
    console.log(error); // when recaptcha canceled it will print captcha canceled
}

the bad part, it may break when google change something in html structure

code of the module

/* global grecaptcha */
import $ from "jquery";

let callback = () => {};
let hideCallback = () => {};

export default function captcha (el, sitekey) {
    const $el = $(el);
    el = $el[0];
    let captchaId = $el.attr("captcha-id");
    let wrapper;
    if (captchaId == null) {
        captchaId = grecaptcha.render(el, {
            sitekey,
            size: "invisible",
            type: "image",
            callback: result => callback(result),
            badge: "inline",
        });
        $(el).attr("captcha-id", captchaId);
    }
    else {
        grecaptcha.reset(captchaId);
    }
    const waitForWrapper = setInterval(() => {
        // first we search for recaptcha iframe
        const iframe = $("iframe").filter((idx, iframe) => iframe.src.includes("recaptcha/api2/bframe"));
        iframe.toArray().some(iframe => {
            const w = $(iframe).closest("body > *");
            // find the corresponding iframe for current captcha
            if (w[0] && !w[0].hasAttribute("captcha-id") || w.attr("captcha-id") == captchaId) {
                w.attr("captcha-id", captchaId);
                wrapper = w; // save iframe wrapper element
                clearInterval(waitForWrapper);
                return true;
            }
        });
    }, 100);
    const result = new Promise((resolve, reject) => {
        callback = (result) => {
            clearInterval(waitForHide);
            resolve(result);
        };
        hideCallback = (result) => {
            clearInterval(waitForHide);
            reject(result);
        };
    });
    grecaptcha.execute(captchaId);
    let shown = false;
    const waitForHide = setInterval(() => {
        if (wrapper) { // if we find iframe wrapper
            if (!shown) {
                // waiting for captcha to show
                if (wrapper.css("visibility") !== "hidden") {
                    shown = true;
                    console.log("shown");
                }
            }
            else {
                // now waiting for it to hide
                if (wrapper.css("visibility") === "hidden") {
                    console.log("hidden");
                    hideCallback(new Error("captcha canceled"));
                }
            }
        }
    }, 100);
    return result;
}
like image 34
ForceUser Avatar answered Nov 09 '22 11:11

ForceUser