Skip to content

Commit

Permalink
Add reCAPTCHA v3 support
Browse files Browse the repository at this point in the history
  • Loading branch information
Christopher Mühl committed May 24, 2019
1 parent 05ac27f commit 310e861
Show file tree
Hide file tree
Showing 17 changed files with 229 additions and 66 deletions.
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,27 @@ A Contao 4 Bundle that replaces Contao's default captcha with the more robust [G
We support the following captchas:
- reCAPTCHA v2 Checkbox
- reCAPTCHA v2 Invisible
- reCAPTCHA v3 (currently work in progress)
- reCAPTCHA v3

### Installation
Either require `dieschittigs/contao-recaptcha` via composer or install the bundle from your Contao Manager.
Either require `dieschittigs/contao-recaptcha` via composer or install the bundle from your Contao Manager. Afterwards you will need to update your database as this bundle adds new fields to some tables.

### Configuration
You can configure the reCAPTCHA type and keys in the Contao system settings.

#### reCAPTCHA v3
reCAPTCHA v3 is more convenient for the end user, but it's also a little more complicated to set up. In addition to configuring your reCAPTCHA keys you will have to set up a global threshold and optionally an individual threshold on a per-form basis.

> reCAPTCHA v3 returns a score (1.0 is very likely a good interaction, 0.0 is very likely a bot). Based on the score, you can take variable action in the context of your site.
> [...]
> reCAPTCHA learns by seeing real traffic on your site. For this reason, scores in a staging environment or soon after implementing may differ from production. As reCAPTCHA v3 doesn't ever interrupt the user flow, you can first run reCAPTCHA without taking action and then decide on thresholds by looking at your traffic in the [admin console](https://g.co/recaptcha/admin). By default, you can use a threshold of 0.5.
(https://developers.google.com/recaptcha/docs/v3#score)

When adding a new captcha form input you will notice an additional textbox for the reCAPTCHA v3 threshold. You can enter a custom threshold that will override the global threshold for this form only. For example you may want to use a higher threshold for password resets than for contact forms.

##### Actions
reCAPTCHA v3 allows you to run the recaptcha code wherever you want with custom action names. This gives you more in-depth information about the scores of users on your page which you can use to fine-tune the threshold. It also allows Google to do better analysis and give more accurate results. If you want to use this feature, you can include the new content element "_Background reCAPTCHA v3_" on individual pages.

#### Fallback
If either the private or public key is left empty, the captcha falls back to the default Contao captcha.
Expand All @@ -16,4 +33,3 @@ We do not perform any additional validation of the keys. If the public key is in

#### CSS
This package is rather unopinionated, so it doesn't provide any CSS. When using Invisible reCAPTCHA Google wants you to display a message on the page to let the user know the form is protected by reCAPTCHA. Google's Javascript already renders this message, but depending on your page layout it could be rendered behind other elements. Therefore it might be necessary to up the `z-index` for the CSS class `grecaptcha-badge`.

12 changes: 10 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dieschittigs/contao-recaptcha",
"description": "Contao 4 Bundle that replaces the default Contao captcha with Google's invisible reCAPTCHA or reCAPTCHA 2.0",
"description": "Contao 4 Bundle that replaces the default Contao captcha with Google's invisible reCAPTCHA, reCAPTCHA 2.0 or reCAPTCHA 3",
"type": "contao-bundle",
"license": "ISC",
"authors": [
Expand All @@ -16,7 +16,15 @@
"autoload": {
"psr-4": {
"DieSchittigs\\RecaptchaBundle\\": "src/"
}
},
"classmap": [
"src/Resources/contao/"
],
"exclude-from-classmap": [
"src/Resources/contao/config/",
"src/Resources/contao/dca/",
"src/Resources/contao/languages/"
]
},
"extra": {
"contao-manager-plugin": "DieSchittigs\\RecaptchaBundle\\ContaoManager\\Plugin"
Expand Down
19 changes: 17 additions & 2 deletions src/Form/FormRecaptcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Contao\FormCaptcha;
use Contao\Config;
use Contao\FormModel;

class FormRecaptcha extends FormCaptcha
{
Expand All @@ -19,11 +20,21 @@ public function __construct($arrAttributes = null)
{
parent::__construct($arrAttributes);

$this->recaptchaType = Config::get('recaptchaType');
$this->publicKey = Config::get('recaptchaPublicKey');
$this->recaptchaType = Config::get('recaptchaType');
$this->publicKey = Config::get('recaptchaPublicKey');
$this->privateKey = Config::get('recaptchaPrivateKey');
$this->globalThreshold = Config::get('recaptcha3GlobalThreshold');

if ($this->recaptcha3_action) {
$this->recaptchaAction = $this->recaptcha3_action;
} elseif ($this->recaptchaType == 'recaptcha3') {
$form = FormModel::findById($this->pid);
$this->recaptchaAction = $form->alias;
}

$this->recaptchaAction = str_replace('-', '_', $this->recaptchaAction);
$this->recaptchaAction = preg_replace('/[^a-zA-Z\/_]/', '', $this->recaptchaAction);

if ($this->useFallback()) {
$this->strTemplate = 'form_captcha';
}
Expand Down Expand Up @@ -71,6 +82,10 @@ public function validate()
// Use this field's threshold, otherwise use the default
$threshold = $this->recaptcha3_threshold ? $this->recaptcha3_threshold : $this->globalThreshold;
if (!$threshold) $threshold = 0;

if ($parsed['action'] && $parsed['action'] != $this->recaptchaAction) {
throw new \Exception;
}

if ($score < $threshold) {
throw new \Exception;
Expand Down
6 changes: 5 additions & 1 deletion src/Resources/contao/config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@

use DieSchittigs\RecaptchaBundle\Form\FormRecaptcha;

$GLOBALS['TL_FFL']['captcha'] = FormRecaptcha::class;
$GLOBALS['TL_FFL']['captcha'] = FormRecaptcha::class;

array_insert($GLOBALS['TL_CTE']['miscellaneous'], 0, [
'backgroundrecaptcha' => Contao\ContentBackgroundRecaptcha::class,
]);
13 changes: 13 additions & 0 deletions src/Resources/contao/dca/tl_content.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

$GLOBALS['TL_DCA']['tl_content']['palettes']['backgroundrecaptcha'] = '{type_legend},type;{description_legend},recaptcha_action;{invisible_legend:hide},invisible,start,stop;';

$GLOBALS['TL_DCA']['tl_content']['fields'] += [
'recaptcha_action' => [
'label' => &$GLOBALS['TL_LANG']['tl_content']['recaptcha_action'],
'exclude' => true,
'inputType' => 'text',
'eval' => ['tl_class' => 'w50', 'required' => true],
'sql' => 'VARCHAR(120) NOT NULL',
],
];
24 changes: 18 additions & 6 deletions src/Resources/contao/dca/tl_form_field.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
<?php

$GLOBALS['TL_DCA']['tl_form_field']['fields']['recaptcha3_threshold'] = [
'label' => &$GLOBALS['TL_LANG']['tl_form_field']['recaptcha3_threshold'],
'inputType' => 'text',
'sql' => "varchar(8) unsigned NOT NULL default '0'",
'eval' => ['tl_class' => 'w50']
];
$GLOBALS['TL_DCA']['tl_form_field']['fields'] += [
'recaptch3_threshold' => [
'label' => &$GLOBALS['TL_LANG']['tl_form_field']['recaptcha3_threshold'],
'inputType' => 'text',
'sql' => "varchar(8) unsigned NOT NULL default ''",
'eval' => ['tl_class' => 'w50 clr'],
],
'recaptcha3_action' => [
'label' => &$GLOBALS['TL_LANG']['tl_form_field']['recaptcha3_action'],
'inputType' => 'text',
'sql' => "varchar(120) unsigned NOT NULL default ''",
'eval' => ['tl_class' => 'w50']
],
];

if (Config::get('recaptchaType') != 'recaptcha3') return;

$GLOBALS['TL_DCA']['tl_form_field']['palettes']['captcha'] = str_replace(',type,label', ',type,label,recaptcha3_threshold,recaptcha3_action', $GLOBALS['TL_DCA']['tl_form_field']['palettes']['captcha']);
66 changes: 36 additions & 30 deletions src/Resources/contao/dca/tl_settings.php
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
<?php

$GLOBALS['TL_DCA']['tl_settings']['fields']['recaptchaType'] = [
'label' => &$GLOBALS['TL_LANG']['tl_settings']['recaptchaType'],
'inputType' => 'select',
'options_callback' => function ()
{
return [
'invisible' => 'reCAPTCHA v2: Invisible',
'recaptcha2' => 'reCAPTCHA v2: Checkbox',
'recaptcha3' => 'reCAPTCHA v3',
];
},
'eval' => ['chosen' => true, 'submitOnChange' => true]
];
$palette = $GLOBALS['TL_DCA']['tl_settings']['palettes'];
$palette = isset($palette['default']) ? $palette['default'] : $palette;

$GLOBALS['TL_DCA']['tl_settings']['fields']['recaptchaPublicKey'] = [
'label' => &$GLOBALS['TL_LANG']['tl_settings']['recaptchaPublicKey'],
'inputType' => 'text',
'eval' => ['tl_class' => 'w50']
];
$GLOBALS['TL_DCA']['tl_settings']['palettes']['default'] = str_replace('{files_legend', '{recaptcha_legend},recaptchaType,recaptchaPublicKey,recaptchaPrivateKey;{files_legend', $palette);
$GLOBALS['TL_DCA']['tl_settings']['palettes']['recaptcha3'] = str_replace('{files_legend', '{recaptcha_legend},recaptchaType,recaptcha3GlobalThreshold,recaptchaPublicKey,recaptchaPrivateKey;{files_legend', $palette);

$GLOBALS['TL_DCA']['tl_settings']['fields']['recaptchaPrivateKey'] = [
'label' => &$GLOBALS['TL_LANG']['tl_settings']['recaptchaPrivateKey'],
'inputType' => 'text',
'eval' => ['tl_class' => 'w50']
];
$GLOBALS['TL_DCA']['tl_settings']['palettes']['__selector__'][] = 'recaptchaType';

$GLOBALS['TL_DCA']['tl_settings']['fields']['recaptcha3GlobalThreshold'] = [
'label' => &$GLOBALS['TL_LANG']['tl_settings']['recaptcha3GlobalThreshold'],
'inputType' => 'text',
'default' => '0.5',
'eval' => ['tl_class' => 'w50']
];

$GLOBALS['TL_DCA']['tl_settings']['palettes'] = str_replace('{files_legend', '{recaptcha_legend},recaptchaType,recaptchaPublicKey,recaptchaPrivateKey;{files_legend', $GLOBALS['TL_DCA']['tl_settings']['palettes']);
$GLOBALS['TL_DCA']['tl_settings']['fields'] += [
'recaptchaType' => [
'label' => &$GLOBALS['TL_LANG']['tl_settings']['recaptchaType'],
'inputType' => 'select',
'options_callback' => function ()
{
return [
'invisible' => 'reCAPTCHA v2: Invisible',
'recaptcha2' => 'reCAPTCHA v2: Checkbox',
'recaptcha3' => 'reCAPTCHA v3',
];
},
'eval' => ['tl_class' => 'w50', 'chosen' => true, 'submitOnChange' => true],
],
'recaptchaPublicKey' => [
'label' => &$GLOBALS['TL_LANG']['tl_settings']['recaptchaPublicKey'],
'inputType' => 'text',
'eval' => ['tl_class' => 'w50 clr'],
],
'recaptchaPrivateKey' => [
'label' => &$GLOBALS['TL_LANG']['tl_settings']['recaptchaPrivateKey'],
'inputType' => 'text',
'eval' => ['tl_class' => 'w50'],
],
'recaptcha3GlobalThreshold' => [
'label' => &$GLOBALS['TL_LANG']['tl_settings']['recaptcha3GlobalThreshold'],
'inputType' => 'text',
'default' => '0.5',
'eval' => ['tl_class' => 'w50'],
],
];
38 changes: 38 additions & 0 deletions src/Resources/contao/elements/ContentBackgroundRecaptcha.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Contao;

class ContentBackgroundRecaptcha extends ContentElement
{
protected $strTemplate = 'ce_background_recaptcha';

protected function compile()
{
if (TL_MODE == 'BE') {
$this->generateBackendOutput();
} else {
$this->generateFrontendOutput();
}
}

protected function generateBackendOutput()
{
$this->strTemplate = 'be_wildcard';
$this->Template = new BackendTemplate($this->strTemplate);

if (Config::get('recaptchaType') == 'recaptcha3') {
$actionString = $GLOBALS['TL_LANG']['tl_content']['recaptcha_action'][0];
$this->Template->wildcard = '<strong>' . $actionString . ':</strong> ' . $this->recaptcha_action;
} else {
$this->Template->wildcard = '<span style="color:red;">' . $GLOBALS['TL_LANG']['tl_content']['recaptcha_wrong_type'] . '</span>';
}
}

protected function generateFrontendOutput()
{
$this->Template->id = $this->id;
$this->Template->recaptchaType = Config::get('recaptchaType');
$this->Template->publicKey = Config::get('recaptchaPublicKey');
$this->Template->action = $this->recaptcha_action;
}
}
3 changes: 3 additions & 0 deletions src/Resources/contao/languages/de/default.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<?php

$GLOBALS['TL_LANG']['ERR']['recaptcha'] = "reCAPTCHA konnte nicht bestätigen, dass Sie kein Roboter sind. Bitte versuchen Sie es noch einmal.";

$GLOBALS['TL_LANG']['CTE']['miscellaneous'] = "Verschiedenes";
$GLOBALS['TL_LANG']['CTE']['backgroundrecaptcha'][0] = "Hintergrund reCAPTCHA v3";
5 changes: 5 additions & 0 deletions src/Resources/contao/languages/de/tl_content.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

$GLOBALS['TL_LANG']['tl_content']['description_legend'] = "reCAPTCHA Einstellungen";
$GLOBALS['TL_LANG']['tl_content']['recaptcha_action'][0] = "reCAPTCHA Aktions-Name";
$GLOBALS['TL_LANG']['tl_content']['recaptcha_wrong_type'] = "Dieses Inhaltselement funktioniert nur, wenn reCAPTCHA v3 ausgewählt ist.";
7 changes: 7 additions & 0 deletions src/Resources/contao/languages/de/tl_form_field.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

$GLOBALS['TL_LANG']['tl_form_field']['recaptcha3_threshold'][0] = "Score-Minimum";
$GLOBALS['TL_LANG']['tl_form_field']['recaptcha3_threshold'][1] = "reCAPTCHA v3 bewertet Nutzeraktionen mit einem Wert zwischen 0 und 1, wobei 1 für einen sehr wahrscheinlich guten Nutzer und 0 für einen Bot steht. Wenn ein globales Minimum gesetzt wird, muss die Bewertung von Google mindestens diesen Wert erreichen, damit das Captcha als Erfolg gezählt wird.";

$GLOBALS['TL_LANG']['tl_form_field']['recaptcha3_action'][0] = "reCaptcha Aktion";
$GLOBALS['TL_LANG']['tl_form_field']['recaptcha3_action'][1] = "Nutzt als Standardwert das Formularalias.";
2 changes: 1 addition & 1 deletion src/Resources/contao/languages/de/tl_settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
$GLOBALS['TL_LANG']['tl_settings']['recaptchaPublicKey'][1] = "Der Schlüssel, der im HTML-Code der Seite ausgegeben wird.";
$GLOBALS['TL_LANG']['tl_settings']['recaptchaPrivateKey'][0] = "Geheimer Schlüssel";
$GLOBALS['TL_LANG']['tl_settings']['recaptchaPrivateKey'][1] = "Der Schlüssel, der für die Kommunikation zwischen Contao und Google verwendet wird.";
$GLOBALS['TL_LANG']['tl_settings']['recaptcha3GlobalThreshold'][0] = "Score-Minimum";
$GLOBALS['TL_LANG']['tl_settings']['recaptcha3GlobalThreshold'][0] = "Globales Score-Minimum";
$GLOBALS['TL_LANG']['tl_settings']['recaptcha3GlobalThreshold'][1] = "reCAPTCHA v3 bewertet Nutzeraktionen mit einem Wert zwischen 0 und 1, wobei 1 für einen sehr wahrscheinlich guten Nutzer und 0 für einen Bot steht. Wenn ein globales Minimum gesetzt wird, muss die Bewertung von Google mindestens diesen Wert erreichen, damit das Captcha als Erfolg gezählt wird.";

$GLOBALS['TL_LANG']['tl_settings']['recaptcha_legend'] = "Sicherheit: Google reCAPTCHA";
3 changes: 3 additions & 0 deletions src/Resources/contao/languages/en/default.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<?php

$GLOBALS['TL_LANG']['ERR']['recaptcha'] = "reCAPTCHA could not verify that you're not a robot. Please try again.";

$GLOBALS['TL_LANG']['CTE']['miscellaneous'] = "Miscellaneous";
$GLOBALS['TL_LANG']['CTE']['backgroundrecaptcha'][0] = "Background reCAPTCHA v3";
5 changes: 5 additions & 0 deletions src/Resources/contao/languages/en/tl_content.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

$GLOBALS['TL_LANG']['tl_content']['description_legend'] = "reCAPTCHA settings";
$GLOBALS['TL_LANG']['tl_content']['recaptcha_action'][0] = "reCAPTCHA action name";
$GLOBALS['TL_LANG']['tl_content']['recaptcha_wrong_type'] = "This content element only works if reCAPTCHA v3 is selected.";
7 changes: 7 additions & 0 deletions src/Resources/contao/languages/en/tl_form_field.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

$GLOBALS['TL_LANG']['tl_form_field']['recaptcha3_threshold'][0] = "Score threshold";
$GLOBALS['TL_LANG']['tl_form_field']['recaptcha3_threshold'][1] = "reCAPTCHA v3 returns a score, based on which you can decide if a user is likely a bot or a human. A score of 1 most likely resembles a human, a score of 0 is most likely a bot. Any captcha request made has to be above this score to be considered safe.";

$GLOBALS['TL_LANG']['tl_form_field']['recaptcha3_action'][0] = "reCaptcha action";
$GLOBALS['TL_LANG']['tl_form_field']['recaptcha3_action'][1] = "Will use the form's alias as a default if not overridden here";
8 changes: 8 additions & 0 deletions src/Resources/contao/templates/ce_background_recaptcha.html5
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php if ($this->recaptchaType != 'recaptcha3') return; ?>

<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoadCallbackModule<?= $this->id ?>&render=<?= $this->publicKey ?>" defer async></script>
<script type="text/javascript" defer async>
function onRecaptchaLoadCallbackModule<?= $this->id ?> () {
grecaptcha.execute('<?= $this->publicKey ?>', {action: '<?= $this->action ?>'})
}
</script>
55 changes: 34 additions & 21 deletions src/Resources/contao/templates/form_recaptcha.html5
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,43 @@
<?php endif; ?>

<div class="g-recaptcha"
id="recaptcha-<?= $this->id ?>"
id="recaptcha-<?= $this->strCaptchaKey ?>"
data-sitekey="<?= $this->publicKey ?>"
data-callback="onSubmit_<?= $this->id ?>"
data-callback="onSubmit_<?= $this->strCaptchaKey ?>"
<?php if ($this->recaptchaType == 'invisible'): ?> data-size="invisible" <?php endif; ?>
>
</div>
></div>

<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoadCallbackField<?= $this->strCaptchaKey ?><?php if ($this->recaptchaType == 'recaptcha3'): ?>&render=<?= $this->publicKey ?><?php endif; ?>" defer async></script>
<script type="text/javascript" defer async>
<?php if ($this->recaptchaType == 'invisible'): ?>
var node = document.getElementById('recaptcha-<?= $this->id ?>');
while (node && node.tagName !== 'FORM') node = node.parentNode;

if (node) {
node.addEventListener('submit', function (e) {
e.preventDefault();
grecaptcha.execute();
});
}

function onSubmit_<?= $this->id ?>() { node.submit(); }
<?php else: ?>
function onSubmit_<?= $this->id ?> () { }
<?php endif; ?>
</script>
function onRecaptchaLoadCallbackField<?= $this->strCaptchaKey ?> () {
var onSubmit_<?= $this->strCaptchaKey ?> = function () { };

<?php if ($this->recaptchaType == 'invisible'): ?>
var node = document.getElementById('recaptcha-<?= $this->strCaptchaKey ?>');
while (node && node.tagName !== 'FORM') node = node.parentNode;

if (node) {
node.addEventListener('submit', function (e) {
e.preventDefault();
grecaptcha.execute();
});
}

<script src="https://www.google.com/recaptcha/api.js" defer async></script>
onSubmit_<?= $this->strCaptchaKey ?> = function () { node.submit(); }
<?php elseif ($this->recaptchaType == 'recaptcha3'): ?>
grecaptcha
.execute('<?= $this->publicKey ?>', {action: '<?= $this->recaptchaAction ?>'})
.then(function (token) {
var node = document.getElementById('recaptcha-<?= $this->strCaptchaKey ?>');

var input = document.createElement('input');
input.setAttribute('type', 'hidden');
input.setAttribute('name', 'g-recaptcha-response');
input.setAttribute('value', token);

node.replaceWith(input);
});
<?php endif; ?>
}
</script>
<?php $this->endblock(); ?>

0 comments on commit 310e861

Please sign in to comment.