diff --git a/README.md b/README.md index 4aa31a9..bd5f1e5 100755 --- a/README.md +++ b/README.md @@ -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. @@ -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`. - diff --git a/composer.json b/composer.json index a6a842d..1a7df29 100644 --- a/composer.json +++ b/composer.json @@ -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": [ @@ -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" diff --git a/src/Form/FormRecaptcha.php b/src/Form/FormRecaptcha.php index c9ed87c..0e7d0d9 100755 --- a/src/Form/FormRecaptcha.php +++ b/src/Form/FormRecaptcha.php @@ -4,6 +4,7 @@ use Contao\FormCaptcha; use Contao\Config; +use Contao\FormModel; class FormRecaptcha extends FormCaptcha { @@ -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'; } @@ -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; diff --git a/src/Resources/contao/config/config.php b/src/Resources/contao/config/config.php index 6147fe0..f8465df 100644 --- a/src/Resources/contao/config/config.php +++ b/src/Resources/contao/config/config.php @@ -2,4 +2,8 @@ use DieSchittigs\RecaptchaBundle\Form\FormRecaptcha; -$GLOBALS['TL_FFL']['captcha'] = FormRecaptcha::class; \ No newline at end of file +$GLOBALS['TL_FFL']['captcha'] = FormRecaptcha::class; + +array_insert($GLOBALS['TL_CTE']['miscellaneous'], 0, [ + 'backgroundrecaptcha' => Contao\ContentBackgroundRecaptcha::class, +]); diff --git a/src/Resources/contao/dca/tl_content.php b/src/Resources/contao/dca/tl_content.php new file mode 100644 index 0000000..9104717 --- /dev/null +++ b/src/Resources/contao/dca/tl_content.php @@ -0,0 +1,13 @@ + [ + 'label' => &$GLOBALS['TL_LANG']['tl_content']['recaptcha_action'], + 'exclude' => true, + 'inputType' => 'text', + 'eval' => ['tl_class' => 'w50', 'required' => true], + 'sql' => 'VARCHAR(120) NOT NULL', + ], +]; diff --git a/src/Resources/contao/dca/tl_form_field.php b/src/Resources/contao/dca/tl_form_field.php index 2f0eb0a..ed61057 100755 --- a/src/Resources/contao/dca/tl_form_field.php +++ b/src/Resources/contao/dca/tl_form_field.php @@ -1,8 +1,20 @@ &$GLOBALS['TL_LANG']['tl_form_field']['recaptcha3_threshold'], - 'inputType' => 'text', - 'sql' => "varchar(8) unsigned NOT NULL default '0'", - 'eval' => ['tl_class' => 'w50'] -]; \ No newline at end of file +$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']); diff --git a/src/Resources/contao/dca/tl_settings.php b/src/Resources/contao/dca/tl_settings.php index ecc829c..2654a8d 100755 --- a/src/Resources/contao/dca/tl_settings.php +++ b/src/Resources/contao/dca/tl_settings.php @@ -1,36 +1,42 @@ &$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'], + ], +]; diff --git a/src/Resources/contao/elements/ContentBackgroundRecaptcha.php b/src/Resources/contao/elements/ContentBackgroundRecaptcha.php new file mode 100644 index 0000000..319a722 --- /dev/null +++ b/src/Resources/contao/elements/ContentBackgroundRecaptcha.php @@ -0,0 +1,38 @@ +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 = '' . $actionString . ': ' . $this->recaptcha_action; + } else { + $this->Template->wildcard = '' . $GLOBALS['TL_LANG']['tl_content']['recaptcha_wrong_type'] . ''; + } + } + + 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; + } +} diff --git a/src/Resources/contao/languages/de/default.php b/src/Resources/contao/languages/de/default.php index f62b87a..a94835e 100644 --- a/src/Resources/contao/languages/de/default.php +++ b/src/Resources/contao/languages/de/default.php @@ -1,3 +1,6 @@ recaptchaType != 'recaptcha3') return; ?> + + + \ No newline at end of file diff --git a/src/Resources/contao/templates/form_recaptcha.html5 b/src/Resources/contao/templates/form_recaptcha.html5 old mode 100644 new mode 100755 index d49c075..f0cf966 --- a/src/Resources/contao/templates/form_recaptcha.html5 +++ b/src/Resources/contao/templates/form_recaptcha.html5 @@ -6,30 +6,43 @@
recaptchaType == 'invisible'): ?> data-size="invisible" - > -
+ > + + function onRecaptchaLoadCallbackFieldstrCaptchaKey ?> () { + var onSubmit_strCaptchaKey ?> = function () { }; + + recaptchaType == 'invisible'): ?> + var node = document.getElementById('recaptcha-strCaptchaKey ?>'); + while (node && node.tagName !== 'FORM') node = node.parentNode; + + if (node) { + node.addEventListener('submit', function (e) { + e.preventDefault(); + grecaptcha.execute(); + }); + } - + onSubmit_strCaptchaKey ?> = function () { node.submit(); } + recaptchaType == 'recaptcha3'): ?> + grecaptcha + .execute('publicKey ?>', {action: 'recaptchaAction ?>'}) + .then(function (token) { + var node = document.getElementById('recaptcha-strCaptchaKey ?>'); + + var input = document.createElement('input'); + input.setAttribute('type', 'hidden'); + input.setAttribute('name', 'g-recaptcha-response'); + input.setAttribute('value', token); + + node.replaceWith(input); + }); + + } + endblock(); ?> \ No newline at end of file