diff --git a/config/install/recaptcha.settings.yml b/config/install/recaptcha.settings.yml index 00ec248..fd003c9 100644 --- a/config/install/recaptcha.settings.yml +++ b/config/install/recaptcha.settings.yml @@ -4,5 +4,5 @@ widget: theme: 'light' type: 'image' size: '' + badge: 'bottomright' tabindex: 0 - noscript: false diff --git a/config/schema/recaptcha.schema.yml b/config/schema/recaptcha.schema.yml index dd90863..5b21dca 100644 --- a/config/schema/recaptcha.schema.yml +++ b/config/schema/recaptcha.schema.yml @@ -23,9 +23,9 @@ recaptcha.settings: size: type: string label: 'Size' + badge: + type: string + label: 'Badge' tabindex: type: integer label: 'Tabindex' - noscript: - type: boolean - label: 'Enable fallback for browsers with JavaScript disabled' diff --git a/js/recaptcha.invisible.js b/js/recaptcha.invisible.js new file mode 100644 index 0000000..da7e4ce --- /dev/null +++ b/js/recaptcha.invisible.js @@ -0,0 +1,106 @@ +/** + * @file + * Invisible reCaptcha behaviors. + */ + +/* globals grecaptcha*/ +/* eslint-disable no-unused-vars*/ +/** + * The submit object that was clicked. + * + * @type {object} + */ +var clickedSubmit; +var clickedSubmitEvent; +var clickedSubmitAjaxEvent; + + +/** + * reCaptcha data-callback that submits the form. + * + */ +function recaptchaOnInvisibleSubmit() { + 'use strict'; + jQuery(clickedSubmit).unbind('.recaptcha'); + if (clickedSubmitAjaxEvent) { + jQuery(clickedSubmit).trigger(clickedSubmitAjaxEvent); + } + else { + jQuery(clickedSubmit).click(); + } + clickedSubmitEvent = clickedSubmit = clickedSubmitAjaxEvent = ''; +} + +(function ($, Drupal) { + 'use strict'; + + /** + * Handles the submission of the form with the invisible reCaptcha. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches the behavior for the invisible reCaptcha. + */ + Drupal.behaviors.invisibleRecaptcha = { + attach: function (context) { + if (Drupal.hasOwnProperty('Ajax')) { + Drupal.Ajax.prototype.beforeSubmit = function (form_values, element, options) { + if ($(this.element).is(clickedSubmit) && grecaptcha.getResponse().length === 0) { + options.needsRevalidate = true; + this.progress.type = 'none'; + clickedSubmitAjaxEvent = this.event; + } + }; + + $(document).ajaxSend(function (event, jqxhr, settings) { + if (settings.needsRevalidate) { + jqxhr.abort(); + $(clickedSubmit).prop('disabled', false); + } + }); + } + $('form', context).each(function () { + var $form = $(this); + if ($form.find('.g-recaptcha[data-size="invisible"]').length) { + $form.find(':submit').on({ + 'mousedown.recaptcha': function (e) { + preventFormSubmit(this, e); + }, + 'click.recaptcha': function (e) { + preventFormSubmit(this, e); + } + }); + } + }); + + /** + * Prevent form submit if recaptcha is not valid. + * + * @param {Object} elem - Triggering element. + * @param {Object} event - Triggering event. + */ + function preventFormSubmit(elem, event) { + if (grecaptcha.getResponse().length === 0) { + // We need validate form, to avoid prevention of html5 validation. + if ($(elem).closest('form')[0].checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + clickedSubmitEvent = event.type; + validateInvisibleCaptcha(elem); + } + } + } + + /** + * Triggers the reCaptcha to validate the form. + * + * @param {object} button - The submit button object was clicked. + */ + function validateInvisibleCaptcha(button) { + clickedSubmit = button; + grecaptcha.execute(); + } + } + }; +})(jQuery, Drupal); diff --git a/migration_templates/d6_recaptcha_settings.yml b/migration_templates/d6_recaptcha_settings.yml index 69ade31..13a946a 100644 --- a/migration_templates/d6_recaptcha_settings.yml +++ b/migration_templates/d6_recaptcha_settings.yml @@ -11,7 +11,6 @@ source: - recaptcha_type - recaptcha_size - recaptcha_tabindex - - recaptcha_noscript process: site_key: recaptcha_site_key secret_key: recaptcha_secret_key @@ -19,7 +18,6 @@ process: 'widget/type': recaptcha_type 'widget/size': recaptcha_size 'widget/tabindex': recaptcha_tabindex - 'widget/noscript': recaptcha_noscript destination: plugin: config config_name: recaptcha.settings diff --git a/migration_templates/d7_recaptcha_settings.yml b/migration_templates/d7_recaptcha_settings.yml index 655cf7c..6371122 100644 --- a/migration_templates/d7_recaptcha_settings.yml +++ b/migration_templates/d7_recaptcha_settings.yml @@ -11,7 +11,6 @@ source: - recaptcha_type - recaptcha_size - recaptcha_tabindex - - recaptcha_noscript process: site_key: recaptcha_site_key secret_key: recaptcha_secret_key @@ -19,7 +18,6 @@ process: 'widget/type': recaptcha_type 'widget/size': recaptcha_size 'widget/tabindex': recaptcha_tabindex - 'widget/noscript': recaptcha_noscript destination: plugin: config config_name: recaptcha.settings diff --git a/recaptcha.libraries.yml b/recaptcha.libraries.yml new file mode 100644 index 0000000..6ecef6d --- /dev/null +++ b/recaptcha.libraries.yml @@ -0,0 +1,7 @@ +recaptcha.invisible: + version: VERSION + js: + js/recaptcha.invisible.js: {} + dependencies: + - core/jquery + - core/drupal \ No newline at end of file diff --git a/recaptcha.module b/recaptcha.module index 674fa36..8b47d9e 100644 --- a/recaptcha.module +++ b/recaptcha.module @@ -42,20 +42,6 @@ function recaptcha_help($route_name, RouteMatchInterface $route_match) { } /** - * Implements hook_theme(). - */ -function recaptcha_theme() { - return [ - 'recaptcha_widget_noscript' => [ - 'variables' => [ - 'widget' => NULL, - ], - 'template' => 'recaptcha-widget-noscript', - ], - ]; -} - -/** * Implements hook_captcha(). */ function recaptcha_captcha($op, $captcha_type = '') { @@ -82,18 +68,6 @@ function recaptcha_captcha($op, $captcha_type = '') { '#value' => 'Google no captcha', ]; - $noscript = ''; - if ($config->get('widget.noscript')) { - $recaptcha_widget_noscript = [ - '#theme' => 'recaptcha_widget_noscript', - '#widget' => [ - 'sitekey' => $recaptcha_site_key, - 'language' => \Drupal::service('language_manager')->getCurrentLanguage()->getId(), - ], - ]; - $noscript = $renderer->render($recaptcha_widget_noscript); - } - $attributes = [ 'class' => 'g-recaptcha', 'data-sitekey' => $recaptcha_site_key, @@ -102,12 +76,19 @@ function recaptcha_captcha($op, $captcha_type = '') { 'data-size' => $config->get('widget.size'), 'data-tabindex' => $config->get('widget.tabindex'), ]; + + // Set the callback for invisible captcha. + if ('invisible' === $config->get('widget.size')) { + $attributes['data-badge'] = $config->get('widget.badge'); + $attributes['data-callback'] = 'recaptchaOnInvisibleSubmit'; + $captcha['form']['#attached']['library'][] = 'recaptcha/recaptcha.invisible'; + } + // Filter out empty tabindex/size. $attributes = array_filter($attributes); $captcha['form']['recaptcha_widget'] = [ '#markup' => '', - '#suffix' => $noscript, '#attached' => [ 'html_head' => [ [ @@ -177,14 +158,3 @@ function recaptcha_captcha_validation($solution, $response, $element, $form_stat } return FALSE; } - -/** - * Process variables for recaptcha-widget-noscript.tpl.php. - * - * @see recaptcha-widget-noscript.tpl.php - */ -function template_preprocess_recaptcha_widget_noscript(&$variables) { - $variables['sitekey'] = $variables['widget']['sitekey']; - $variables['language'] = $variables['widget']['language']; - $variables['url'] = Url::fromUri('https://www.google.com/recaptcha/api/fallback', ['query' => ['k' => $variables['widget']['sitekey'], 'hl' => $variables['widget']['language']], 'absolute' => TRUE])->toString(); -} diff --git a/src/Form/ReCaptchaAdminSettingsForm.php b/src/Form/ReCaptchaAdminSettingsForm.php index fd2ca6d..2b25f3e 100644 --- a/src/Form/ReCaptchaAdminSettingsForm.php +++ b/src/Form/ReCaptchaAdminSettingsForm.php @@ -87,10 +87,27 @@ class ReCaptchaAdminSettingsForm extends ConfigFormBase { '#options' => [ '' => $this->t('Normal (default)'), 'compact' => $this->t('Compact'), + 'invisible' => $this->t('Invisible'), ], '#title' => $this->t('Size'), '#type' => 'select', ]; + $form['widget']['recaptcha_badge'] = [ + '#default_value' => $config->get('widget.badge'), + '#description' => $this->t('Reposition the reCAPTCHA badge. "Inline" allows you to control the CSS.'), + '#options' => [ + 'bottomright' => $this->t('Bottom Right (default)'), + 'bottomleft' => $this->t('Bottom Left'), + 'inline' => $this->t('Inline'), + ], + '#title' => $this->t('Badge'), + '#type' => 'select', + '#states' => array( + 'visible' => array( + ':input[name="recaptcha_size"]' => array('value' => 'invisible'), + ), + ), + ]; $form['widget']['recaptcha_tabindex'] = [ '#default_value' => $config->get('widget.tabindex'), '#description' => $this->t('Set the tabindex of the widget and challenge (Default = 0). If other elements in your page use tabindex, it should be set to make user navigation easier.', [':tabindex' => Url::fromUri('http://www.w3.org/TR/html4/interact/forms.html', ['fragment' => 'adef-tabindex'])->toString()]), @@ -99,12 +116,6 @@ class ReCaptchaAdminSettingsForm extends ConfigFormBase { '#type' => 'number', '#min' => -1, ]; - $form['widget']['recaptcha_noscript'] = [ - '#default_value' => $config->get('widget.noscript'), - '#description' => $this->t('If JavaScript is a requirement for your site, you should not enable this feature. With this enabled, a compatibility layer will be added to the captcha to support non-js users.'), - '#title' => $this->t('Enable fallback for browsers with JavaScript disabled'), - '#type' => 'checkbox', - ]; return parent::buildForm($form, $form_state); } @@ -120,8 +131,8 @@ class ReCaptchaAdminSettingsForm extends ConfigFormBase { ->set('widget.theme', $form_state->getValue('recaptcha_theme')) ->set('widget.type', $form_state->getValue('recaptcha_type')) ->set('widget.size', $form_state->getValue('recaptcha_size')) + ->set('widget.badge', $form_state->getValue('recaptcha_badge')) ->set('widget.tabindex', $form_state->getValue('recaptcha_tabindex')) - ->set('widget.noscript', $form_state->getValue('recaptcha_noscript')) ->save(); parent::submitForm($form, $form_state); diff --git a/src/Tests/ReCaptchaBasicTest.php b/src/Tests/ReCaptchaBasicTest.php index 9d9ca8f..adc2ebc 100644 --- a/src/Tests/ReCaptchaBasicTest.php +++ b/src/Tests/ReCaptchaBasicTest.php @@ -128,13 +128,6 @@ class ReCaptchaBasicTest extends WebTestBase { $this->assertRaw('', '[testReCaptchaOnLoginForm]: reCAPTCHA is shown on form.'); $this->assertNoRaw($grecaptcha . '