Ben jij geen robot? Ik ook niet. Toch moet ik het keer op keer bewijzen. We kennen het vast wel inmiddels. Formulier invullen? Ik ben geen robot aanvinken. Wat mij opvalt, is dat het tegenwoordig bijna altijd tot het oplossen van een puzzel leidt. Ik krijg vrijwel nooit meer direct een groen vinkje! Was het maar een leuke bezigheid. Nee, alleen maar irritant. En het meest irritante nog wel: Je lost em op, je weet zeker dat je het goed hebt. Nee hoor, fout, doe er nog maar 1. Dat met die auto's op de foto en bussen en verkeersborden etc. Na 2 keer mag je dan toch door. Fijn hoor.

Vanuit mijn werk is het ook nog eens helemaal irritant voor onze bezoekers met een iOS apparaat. Er zit een bug in de code van reCAPTCHA v2, waarbij de pagina lekker naar beneden wordt gescrollt. Poef, daar zit je dan in de footer van de website. Succes ermee... Zo gaat je conversieratio wel naar de knoppen ja.

Deze bug bestaat al een aantal jaar, en doet zich bij verschillende versies van iOS voor. Soms is het er wel, dan weer niet, dan weer wel. Denk je dat Google er iets aan doet? Nee hoor. Je kan met wat suggesties van de github community wel wat scriptjes proberen, maar die bleken niet te helpen bij de recente versie van iOS. Hieronder een voorbeeld oplossing:

var HEADER_HEIGHT = 0; // Height of header/menu fixed if exists
var isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
var grecaptchaPosition;

var isScrolledIntoView = function (elem) {
  var elemRect = elem.getBoundingClientRect();
  var isVisible = (elemRect.top - HEADER_HEIGHT >= 0 && elemRect.bottom <= window.innerHeight);

  return isVisible;
};

if (isIOS) {
  var recaptchaElements = document.querySelectorAll('.g-recaptcha');

  window.addEventListener('scroll', function () {
    Array.prototype.forEach.call(recaptchaElements, function (element) {
      if (isScrolledIntoView(element)) {
        grecaptchaPosition = document.documentElement.scrollTop || document.body.scrollTop;
      }
    });
  }, false);
}

var onReCaptchaSuccess = function () {
  if (isIOS && grecaptchaPosition !== undefined) {
    window.scrollTo(0, grecaptchaPosition);
  }
};
<div class="g-recaptcha" data-theme="light" data-type="image" data-sitekey="XXXXXX"></div>

Mocht dit nu niet je probleem verhelpen, dan is er altijd nog reCAPTCHA v3! (of invisible reCAPTCHA v2).

Deze nieuwe versie is eind Oktober 2018 uit de beta gekomen en bevat geen vinkvakjes meer en captcha challenges zoals we deze maar al te goed kennen.

Het enige wat deze versie aan irritatiefactor heeft, net als de invisble variant van versie 2, is die badge van Google die vertoont 'moet' worden. Dat staat in de gebruikersovereenkomst.

Ik snap het wel vanuit Google, maar dat ding zit op de meest irritante plekken, waar je vaak ook al andere knoppen op je site hebt gehangen. Ja, je kan het aanpassen door de badge data-badge=inline mee te geven, dat klopt, echter dat werkt alleen bij versie v2 met de invisible captcha. En die bevat nog van die puzzels/challenges.

Maar wacht, versie 3 werkt dus anders en heeft geen challenges die de gebruiker lastig vallen! AWESOME, toch? Ja, maar alleen de badge inline werkt(e) een paar weken geleden (nog) niet. Laat dat nou net die irritatie bij mij zijn haha.

Ik wil die badge gewoon inline kunnen stylen en ergens in de footer dumpen.

Nah ja, helaas. Wat wel goed is aan versie 3, is dat je dus geen reCAPTCHA challenges meer krijgt. Hoe werkt dat dan? Je draait gewoon een stukje code met een specifieke reCAPTCHA sleutel die je voor versie 3 hebt aangemaakt in de reCAPTCHA admin sectie.

Hierdoor weet Google eigenlijk al welke versie er moet worden aangeboden. En nu het vernuftige aan deze versie: Google monitort het gedrag van de bezoeker op jouw aangeven, en analyseert dit op de achtergrond. Zodra jij een validatie doet richting reCAPTCHA (dus bijvoorbeeld wanneer er op verzenden wordt geklikt bij een formulier), dan heeft Google in de tussentijd het gedrag al in de gaten gehouden en geeft een score tussen de 0 en 1 terug. Jij kan dan aangeven, als het een 0.5 of lager is, dan moet er geen formulier verzonden worden. Google weet met 0.9 eigenlijk zo goed als zeker dat het geen spam-bot is. Bij 0.3 zou je toch je moeten afvragen of dit niet een spam-bot is.

Mooi, handig, makkelijk, geen last van als gebruiker, maar wel een beetje kantje boord met privacy en het idee dat je gedragingen nog meer gemonitort worden door Google. En dat laatste maakt het een vernuftige zet van Google. Zij meer data, jij minder irritatie.

Behalve dan die BADGE! Okay, die is persoonlijk...

Goed, hoe doe je dit dan in de praktijk? De aangeleverde code is niet geweldig, en de dev-docs zijn ook nog niet helemaal op orde, maar hieronder hoe je het in werking zet:

Allereerst moet je sowieso de recaptcha api aanroepen op je pagina's:

<script src='https://www.google.com/recaptcha/api.js?render=<jouw publieke v3 recaptcha key>'></script>

Daarna moet je ervoor zorgen dat er zodra het ingeladen is, een pageview wordt doorgegeven:

<script>
  grecaptcha.ready(function() {
      grecaptcha.execute('<jouw publieke v3 recaptcha key>', {action: 'homepage'}).then(function(token) {
         ...
      });
  });
  </script>

Ik raad dan aan een action:'pageview' mee te geven op wanneer je een CMS hebt dat met templates werkt. En een 'homepage' op alleen de homepage).

Waar de drie puntjes staan, ..., kan je dan een validatie POST functie plaatsen of aanroepen wanneer het nodig is dat er gecontroleerd wordt. Dit controleren doe je om in te grijpen.

Dan denk ik dus als meest voor de hand liggende controle moment, bij het verzenden van een formulier. Dat zal ik hieronder dan nog even verder uitwerken. Waar je ook aan kan denken, is bij het klikken van een hyperlink. Nou zullen bots die niet per se een click event triggeren, maar je zou wellicht wel een trigger kunnen schrijven voor dit soort spiders, en ze daarmee blokkeren. Moet je alleen niet bij de goedaardige spiders doen die je content ranken 😉

Anyways, hier een mogelijke functie voor bij een click op de submit button:

document.getElementById("btnSubmit").addEventListener("click", function (e) {
    e = "<public key hier invullen>";
    grecaptcha.ready(function () {
        grecaptcha.execute(e, {
            action: "submit"
        }).then(function (e) {
            jQuery.ajax({
                type: "POST",
                url: "https://www.jouwdomein.nl/recaptcha-verify-v3.php?token=" + e,
                data: JSON.stringify(e),
                dataType: "json",
                success: function (e) {
                    if ("true" == e.success) {
                        var t = document.getElementsByTagName("form")[0];
                        t.setAttribute("id", t.name), document.getElementById(t.name).submit()
                    }
                },
                error: function (e, t, n) {
                    console.log(n)
                }
            })
        })
    })
});

De jQuery AJAX call post de info naar een php bestand wat je kan gebruiken om via Google een score terug te krijgen. Deze zijn gewoon te krijgen op github, en kan je gebruiken en een beetje ombouwen:

<?php
/**
 * @copyright Copyright (c) 2015, Google Inc.
 * @link      https://www.google.com/recaptcha
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
require __DIR__ . '/appengine-https.php';
// Initiate the autoloader. The file should be generated by Composer.
// You will provide your own autoloader or require the files directly if you did
// not install via Composer.
require_once __DIR__ . '/../vendor/autoload.php';
// Register API keys at https://www.google.com/recaptcha/admin
$siteKey = '';
$secret = '';
// Copy the config.php.dist file to config.php and update it with your keys to run the examples
if ($siteKey == '' && is_readable(__DIR__ . '/config.php')) {
    $config = include __DIR__ . '/config.php';
    $siteKey = $config['v3']['site'];
    $secret = $config['v3']['secret'];
}
// Effectively we're providing an API endpoint here that will accept the token, verify it, and return the action / score to the page
// In production, always sanitize and validate the input you retrieve from the request.
$recaptcha = new \ReCaptcha\ReCaptcha($secret);
$resp = $recaptcha->setExpectedHostname($_SERVER['SERVER_NAME'])
                  ->setExpectedAction($_GET['action'])
                  ->setScoreThreshold(0.5)
                  ->verify($_GET['token'], $_SERVER['REMOTE_ADDR']);
header('Content-type:application/json');
echo json_encode($resp->toArray());

Let wel dat je dus nog even in de config.php je keys moet aanleveren. Hier wordt ook de secret key van reCAPTCHA gebruikt om het gedrag te verifiëren. de setScoreThreshold(0.5) is dan iets waar je mee kan spelen. Zet em wat lager en je weet zeker dat alle mensen er in ieder geval doorkomen. Sommige spam zou ook nog door kunnen komen. Zet em hoger en mensen met automatische formulier invullers komen misschien niet zomaar er doorheen.

Dusssss je hoort em niet, maar ziet em wel, een beetje dan... door die lelijke badge (Ja ik weet hoe ik het kan manipuleren, maar goed dat is niet netjes en hoort niet).

UPDATE (17-4-2019): Inmiddels mag je van Google de batch verbergen, mits je maar de disclaimer en privacy linkjes en teksten in de user flow plaatst. Dat is toch wel een stukje fijner dacht ik zo!