最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Is there any recaptcha v2 close event? - Stack Overflow

programmeradmin4浏览0评论

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

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

Share Improve this question edited Jun 15, 2017 at 15:50 ForceUser asked Jun 15, 2017 at 15:45 ForceUserForceUser 1,1391 gold badge15 silver badges23 bronze badges 2
  • Did you ever find a solution to this? I can't find an appropriate event in the docs either... – Henders Commented Nov 10, 2017 at 10:05
  • my solution was to set up timer and wait when recatpcha iframe bees hidden, i will post my answer soon – ForceUser Commented Nov 10, 2017 at 19:56
Add a ment  | 

3 Answers 3

Reset to default 2

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

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

I created a dom observer to detect when captcha is attached to DOM, then I disconnect it (because it is no longer needed) and add click handler to its background element.

Keep in mind that this solution is sensitive to any changes in DOM structure, so if google decides to change it for whatever reason, it may break.

Also remember to cleanup the observers/listeners, in my case (react) I do it in cleanup function of useEffect.

    const captchaBackgroundClickHandler = () => {
        ...do whatever you need on captcha cancel
    };

    const domObserver = new MutationObserver(() => {
        const iframe = document.querySelector("iframe[src^=\"https://www.google./recaptcha\"][src*=\"bframe\"]");

        if (iframe) {
            domObserver.disconnect();

            captchaBackground = iframe.parentNode?.parentNode?.firstChild;
            captchaBackground?.addEventListener("click", captchaBackgroundClickHandler);
        }
    });

    domObserver.observe(document.documentElement || document.body, { childList: true, subtree: true });
发布评论

评论列表(0)

  1. 暂无评论