Skip to content

Instantly share code, notes, and snippets.

@sean-e-dietrich
Created August 7, 2019 20:44
Show Gist options
  • Select an option

  • Save sean-e-dietrich/5defdf8892f9d565b4c9ab0924b069af to your computer and use it in GitHub Desktop.

Select an option

Save sean-e-dietrich/5defdf8892f9d565b4c9ab0924b069af to your computer and use it in GitHub Desktop.
Recaptch w/ Invisible and Ajax Issue
diff --git a/config/install/captcha.captcha_point.recaptcha_test_ajax_form.yml b/config/install/captcha.captcha_point.recaptcha_test_ajax_form.yml
new file mode 100644
index 0000000..c630a71
--- /dev/null
+++ b/config/install/captcha.captcha_point.recaptcha_test_ajax_form.yml
@@ -0,0 +1,10 @@
+langcode: en
+status: true
+dependencies: { }
+formId: recaptcha_test_ajax_form
+captchaType: recaptcha/reCAPTCHA
+label: recaptcha_test_ajax_form
+dependencies:
+ enforced:
+ module:
+ - recaptcha_test
diff --git a/js/recaptcha.invisible.js b/js/recaptcha.invisible.js
new file mode 100644
index 0000000..589ab00
--- /dev/null
+++ b/js/recaptcha.invisible.js
@@ -0,0 +1,128 @@
+/**
+ * @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(token) {
+ 'use strict';
+
+ jQuery(clickedSubmit).parents('form').find('.g-recaptcha-response').val(token);
+
+ 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')) {
+ var originalBeforeSubmit = Drupal.Ajax.prototype.beforeSubmit;
+ Drupal.Ajax.prototype.beforeSubmit = function (form_values, element, options) {
+ if (this.event === 'mousedown' && $(this.element).data('recaptcha-submit') && grecaptcha.getResponse().length === 0) {
+ clickedSubmit = this.element;
+ options.needsRevalidate = true;
+ this.progress.type = 'none';
+ clickedSubmitAjaxEvent = this.event;
+ }
+
+ originalBeforeSubmit.apply(this, arguments);
+ };
+
+ if (!$(document).data('invisible-recaptcha-ajax-send-processed')) {
+ $(document).ajaxSend(function (event, jqxhr, settings) {
+ if (settings.needsRevalidate) {
+ jqxhr.abort();
+ $(clickedSubmit).prop('disabled', false);
+ }
+ });
+
+ $(document).data('invisible-recaptcha-ajax-send-processed', true);
+ }
+ }
+ $('form', context).each(function () {
+ var $form = $(this);
+ if ($form.find('.g-recaptcha[data-size="invisible"]').length) {
+ $form.find(':submit').data('recaptcha-submit', true).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.
+ var form = $(elem).closest('form')[0];
+ if (form && typeof form.checkValidity === 'function' && !$(form).attr('validate')) {
+ if (form.checkValidity()) {
+ event.preventDefault();
+ event.stopPropagation();
+ clickedSubmitEvent = event.type;
+ validateInvisibleCaptcha(elem);
+ }
+ else {
+ if (typeof form.reportValidity === 'function') {
+ form.reportValidity();
+ }
+ }
+ }
+ else {
+ 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/js/recaptcha.js b/js/recaptcha.js
new file mode 100644
index 0000000..a9a5912
--- /dev/null
+++ b/js/recaptcha.js
@@ -0,0 +1,34 @@
+/**
+ * @file
+ * Contains the definition of the behaviour recaptcha.
+ */
+
+(function ($, Drupal) {
+ Drupal.behaviors.recaptcha = {
+ attach: function (context) {
+ $('.g-recaptcha', context).each(function () {
+ if (typeof grecaptcha === 'undefined' || typeof grecaptcha.render !== 'function') {
+ return;
+ }
+ if ($(this).closest('body').length > 0) {
+ if ($(this).hasClass('recaptcha-processed')) {
+ grecaptcha.reset();
+ }
+ else {
+ grecaptcha.render(this, $(this).data());
+ $(this).addClass('recaptcha-processed');
+ }
+ }
+ });
+ }
+ };
+
+ window.drupalRecaptchaOnload = function () {
+ $('.g-recaptcha').each(function () {
+ if (!$(this).hasClass('recaptcha-processed')) {
+ grecaptcha.render(this, $(this).data());
+ $(this).addClass('recaptcha-processed');
+ }
+ });
+ };
+})(jQuery, Drupal);
diff --git a/recaptcha.libraries.yml b/recaptcha.libraries.yml
new file mode 100644
index 0000000..e334a34
--- /dev/null
+++ b/recaptcha.libraries.yml
@@ -0,0 +1,13 @@
+recaptcha:
+ js:
+ js/recaptcha.js: {}
+ dependencies:
+ - core/drupal
+ - core/jquery
+
+recaptcha.invisible:
+ js:
+ js/recaptcha.invisible.js: {}
+ dependencies:
+ - core/jquery
+ - core/drupal
diff --git a/recaptcha.module b/recaptcha.module
index cc1953e..ec7f8b0 100644
--- a/recaptcha.module
+++ b/recaptcha.module
@@ -89,16 +89,14 @@ function recaptcha_captcha($op, $captcha_type = '') {
// captcha type can be displayed on cached pages.
$captcha['cacheable'] = TRUE;
- // Check if reCAPTCHA use globally is enabled.
- $recaptcha_src = 'https://www.google.com/recaptcha/api.js';
- $recaptcha_src_fallback = 'https://www.google.com/recaptcha/api/fallback';
- if ($recaptcha_use_globally) {
- $recaptcha_src = 'https://www.recaptcha.net/recaptcha/api.js';
- $recaptcha_src_fallback = 'https://www.recaptcha.net/recaptcha/api/fallback';
- }
-
$noscript = '';
if ($config->get('widget.noscript')) {
+ // Check if reCAPTCHA use globally is enabled.
+ $recaptcha_src_fallback = 'https://www.google.com/recaptcha/api/fallback';
+ if ($recaptcha_use_globally) {
+ $recaptcha_src_fallback = 'https://www.recaptcha.net/recaptcha/api/fallback';
+ }
+
$recaptcha_widget_noscript = [
'#theme' => 'recaptcha_widget_noscript',
'#widget' => [
@@ -118,6 +116,7 @@ function recaptcha_captcha($op, $captcha_type = '') {
'data-size' => $config->get('widget.size'),
'data-tabindex' => $config->get('widget.tabindex'),
];
+
// Filter out empty tabindex/size.
$attributes = array_filter($attributes);
@@ -125,20 +124,15 @@ function recaptcha_captcha($op, $captcha_type = '') {
'#markup' => '<div' . new Attribute($attributes) . '></div>',
'#suffix' => $noscript,
'#attached' => [
- 'html_head' => [
- [
- [
- '#tag' => 'script',
- '#attributes' => [
- 'src' => Url::fromUri($recaptcha_src, ['query' => ['hl' => \Drupal::service('language_manager')->getCurrentLanguage()->getId()], 'absolute' => TRUE])->toString(),
- 'async' => TRUE,
- 'defer' => TRUE,
- ],
- ],
- 'recaptcha_api',
- ],
+ 'library' => [
+ 'recaptcha/recaptcha',
+ 'recaptcha/google.recaptcha_' . \Drupal::service('language_manager')->getCurrentLanguage()->getId(),
],
],
+ '#cache' => [
+ 'tags' => [ 'library_info' ],
+ 'contexts' => [ 'languages' ],
+ ],
];
}
else {
@@ -153,6 +147,46 @@ function recaptcha_captcha($op, $captcha_type = '') {
}
}
+/**
+ * Implements hook_library_info_build().
+ */
+function recaptcha_library_info_build() {
+ $libraries = [];
+ $languages = \Drupal::service('language_manager')->getLanguages();
+ $config = \Drupal::config('recaptcha.settings');
+ $use_globally = $config->get('use_globally');
+ $default_src = $recaptcha_src = 'https://www.google.com/recaptcha/api.js';
+ if ($use_globally) {
+ $recaptcha_src = 'https://www.recaptcha.net/recaptcha/api.js';
+ }
+
+ foreach ($languages as $lang => $language) {
+ $url = Url::fromUri($recaptcha_src, [
+ 'query' => [
+ 'hl' => $lang,
+ 'render' => 'explicit',
+ 'onload' => 'drupalRecaptchaOnload',
+ ],
+ 'absolute' => TRUE,
+ ])->toString();
+ $libraries["google.recaptcha_$lang"] = [
+ 'version' => '1.x',
+ 'header' => TRUE,
+ 'js' => [
+ $url => [
+ 'type' => 'external',
+ 'minified' => TRUE,
+ 'attributes' => [
+ 'async' => TRUE,
+ 'defer' => TRUE,
+ ],
+ ],
+ ],
+ ];
+ }
+ return $libraries;
+ }
+
/**
* CAPTCHA Callback; Validates the reCAPTCHA code.
*/
@@ -219,5 +253,11 @@ function recaptcha_captcha_validation($solution, $response, $element, $form_stat
function template_preprocess_recaptcha_widget_noscript(&$variables) {
$variables['sitekey'] = $variables['widget']['sitekey'];
$variables['language'] = $variables['widget']['language'];
- $variables['url'] = Url::fromUri($variables['widget']['recaptcha_src_fallback'], ['query' => ['k' => $variables['widget']['sitekey'], 'hl' => $variables['widget']['language']], 'absolute' => TRUE])->toString();
+ $variables['url'] = Url::fromUri($variables['widget']['recaptcha_src_fallback'], [
+ 'query' => [
+ 'k' => $variables['widget']['sitekey'],
+ 'hl' => $variables['widget']['language'],
+ ],
+ 'absolute' => TRUE,
+ ])->toString();
}
diff --git a/recaptcha.services.yml b/recaptcha.services.yml
new file mode 100644
index 0000000..93819ed
--- /dev/null
+++ b/recaptcha.services.yml
@@ -0,0 +1,6 @@
+services:
+ recaptcha.config_subscriber:
+ class: Drupal\recaptcha\EventSubscriber\RecaptchaSettingsConfigSubscriber
+ arguments: ['@cache_tags.invalidator']
+ tags:
+ - { name: event_subscriber }
diff --git a/src/EventSubscriber/RecaptchaSettingsConfigSubscriber.php b/src/EventSubscriber/RecaptchaSettingsConfigSubscriber.php
new file mode 100644
index 0000000..ae216a7
--- /dev/null
+++ b/src/EventSubscriber/RecaptchaSettingsConfigSubscriber.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\recaptcha\EventSubscriber;
+
+use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
+use Drupal\Core\Config\ConfigCrudEvent;
+use Drupal\Core\Config\ConfigEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * A subscriber rebuilding the library info when the use_globally setting is
+ * updated.
+ */
+class RecaptchaSettingsConfigSubscriber implements EventSubscriberInterface {
+
+ /**
+ * The cache tags invalidator.
+ *
+ * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
+ */
+ protected $cacheTagsInvalidator;
+
+ /**
+ * Constructs a RecaptchaSettingsConfigSubscriber object.
+ *
+ * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
+ * The cache tags invalidator.
+ */
+ public function __construct(CacheTagsInvalidatorInterface $cache_tags_invalidator) {
+ $this->cacheTagsInvalidator = $cache_tags_invalidator;
+ }
+
+ /**
+ * Invalidate the library_info tag when the value of
+ * recaptcha.settings use_globally is changed.
+ *
+ * @param \Drupal\Core\Config\ConfigCrudEvent $event
+ * The Event to process.
+ */
+ public function onSave(ConfigCrudEvent $event) {
+ if ($event->getConfig()->getName() === 'recaptcha.settings') {
+ if ($event->isChanged('use_globally')) {
+ $this->cacheTagsInvalidator->invalidateTags(['library_info']);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ $events[ConfigEvents::SAVE][] = ['onSave'];
+ return $events;
+ }
+
+}
diff --git a/src/Form/RecaptchaTestAjaxForm.php b/src/Form/RecaptchaTestAjaxForm.php
new file mode 100644
index 0000000..70044b2
--- /dev/null
+++ b/src/Form/RecaptchaTestAjaxForm.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\recaptcha_test\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+class RecaptchaTestAjaxForm extends FormBase {
+
+ use StringTranslationTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'recaptcha_test_ajax_form';
+ }
+
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $form = [];
+
+ $form['messages'] = [
+ '#type' => 'status_messages',
+ ];
+
+ $form['email'] = [
+ '#type' => 'email',
+ '#title' => $this->t('Email'),
+ '#required' => TRUE,
+ ];
+
+ $form['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ '#validate' => ['::validateForm'],
+ '#ajax' => [
+ 'callback' => '::ajaxCallback',
+ 'wrapper' => 'recaptcha-test-ajax-form-wrapper',
+ ],
+ ];
+
+ $form['#prefix'] = '<div id="recaptcha-test-ajax-form-wrapper">';
+ $form['#suffix'] = '</div>';
+ return $form;
+ }
+
+ public function ajaxCallback(array &$form, FormStateInterface $form_state) {
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $email = $form_state->getValue('email');
+ if ($email == 'invalid@example.com') {
+ $form_state->setError($form['email'], 'Invalid email');
+ }
+
+ }
+
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $this->messenger()->addStatus('Form submit successful.');
+ }
+
+
+}
diff --git a/templates/recaptcha-widget-noscript.html.twig b/templates/recaptcha-widget-noscript.html.twig
deleted file mode 100644
index b1737fe..0000000
--- a/templates/recaptcha-widget-noscript.html.twig
+++ /dev/null
@@ -1,29 +0,0 @@
-{#
-/**
- * @file recaptcha-widget-noscript.tpl.php
- * Default theme implementation to present the reCAPTCHA noscript code.
- *
- * Available variables:
- * - sitekey: Google web service site key.
- * - language: Current site language code.
- * - url: Google web service API url.
- *
- * @see template_preprocess()
- * @see template_preprocess_recaptcha_widget_noscript()
- *
- * @ingroup themeable
- */
-#}
-
-<noscript>
- <div style="width: 302px; height: 352px;">
- <div style="width: 302px; height: 352px; position: relative;">
- <div style="width: 302px; height: 352px; position: absolute;">
- <iframe src="{{ url }}" frameborder="0" scrolling="no" style="width: 302px; height:352px; border-style: none;"></iframe>
- </div>
- <div style="width: 250px; height: 80px; position: absolute; border-style: none; bottom: 21px; left: 25px; margin: 0px; padding: 0px; right: 25px;">
- <textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 80px; border: 1px solid #c1c1c1; margin: 0px; padding: 0px; resize: none;" value=""></textarea>
- </div>
- </div>
- </div>
-</noscript>
diff --git a/tests/modules/recaptcha_test/recaptcha_test.info.yml b/tests/modules/recaptcha_test/recaptcha_test.info.yml
new file mode 100644
index 0000000..48b9a75
--- /dev/null
+++ b/tests/modules/recaptcha_test/recaptcha_test.info.yml
@@ -0,0 +1,7 @@
+name: 'reCAPTCHA Test'
+type: module
+description: 'Test module for the recaptcha module.'
+package: Testing
+core: 8.x
+dependencies:
+ - recaptcha
diff --git a/tests/modules/recaptcha_test/recaptcha_test.routing.yml b/tests/modules/recaptcha_test/recaptcha_test.routing.yml
new file mode 100644
index 0000000..9461ff3
--- /dev/null
+++ b/tests/modules/recaptcha_test/recaptcha_test.routing.yml
@@ -0,0 +1,23 @@
+recaptcha_test.page:
+ path: '/recaptcha-test'
+ defaults:
+ _controller: '\Drupal\recaptcha_test\Controller\RecaptchaTestAjaxFormController::button'
+ _title: 'reCAPTCHA Test Ajax Page'
+ requirements:
+ _access: 'TRUE'
+
+recaptcha_test.ajax:
+ path: '/recaptcha-test/ajax'
+ defaults:
+ _controller: '\Drupal\recaptcha_test\Controller\RecaptchaTestAjaxFormController::ajaxForm'
+ _title: 'reCAPTCHA Test Ajax Controller'
+ requirements:
+ _access: 'TRUE'
+
+recaptcha_test.form:
+ path: '/recaptcha-test/form'
+ defaults:
+ _form: '\Drupal\recaptcha_test\Form\RecaptchaTestAjaxForm'
+ _title: 'reCAPTCHA Test Ajax Form'
+ requirements:
+ _access: 'TRUE'
diff --git a/tests/modules/recaptcha_test/src/Controller/RecaptchaTestAjaxFormController.php b/tests/modules/recaptcha_test/src/Controller/RecaptchaTestAjaxFormController.php
new file mode 100644
index 0000000..6109024
--- /dev/null
+++ b/tests/modules/recaptcha_test/src/Controller/RecaptchaTestAjaxFormController.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\recaptcha_test\Controller;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class RecaptchaTestAjaxFormController extends ControllerBase implements ContainerInjectionInterface {
+
+ public function __construct(FormBuilderInterface $form_builder) {
+ $this->formBuilder = $form_builder;
+ }
+
+ public static function create(ContainerInterface $container){
+ return new static(
+ $container->get('form_builder')
+ );
+ }
+
+ public function button() {
+ $output = [];
+
+ $output['container'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'id' => 'recaptcha-test-container',
+ ]
+ ];
+
+ $url = Url::fromRoute('recaptcha_test.ajax', []);
+
+ $output['container']['ajax_link'] = [
+ '#id' => 'load-ajax-form',
+ '#type' => 'link',
+ '#title' => $this->t('Load Ajax Form'),
+ '#url' => $url,
+ '#attributes' => [
+ 'class' => ['use-ajax', 'button', 'secondary', 'btn', 'btn-secondary'],
+ ],
+ ];
+
+ $output['#attached']['library'][] = 'core/drupal.ajax';
+
+ // @see https://api.drupal.org/api/drupal/core%21core.api.php/group/ajax/8.2.x
+ return $output;
+ }
+
+ /**
+ * Ajax callback returning a form.
+ *
+ * @return AjaxResponse
+ */
+ public function ajaxForm() {
+ $form = $this->formBuilder->getForm('Drupal\recaptcha_test\Form\RecaptchaTestAjaxForm');
+
+ $ajax = new AjaxResponse();
+ $ajax->addCommand(new ReplaceCommand('#recaptcha-test-container', $form));
+ return $ajax;
+ }
+
+}
diff --git a/tests/src/Functional/ReCaptchaBasicTest.php b/tests/src/Functional/ReCaptchaBasicTest.php
index daf01cc..cb096f9 100644
--- a/tests/src/Functional/ReCaptchaBasicTest.php
+++ b/tests/src/Functional/ReCaptchaBasicTest.php
@@ -110,20 +110,22 @@ class ReCaptchaBasicTest extends BrowserTestBase {
public function testReCaptchaOnLoginForm() {
$site_key = $this->randomMachineName(40);
$secret_key = $this->randomMachineName(40);
- $grecaptcha = '<div class="g-recaptcha" data-sitekey="' . $site_key . '" data-theme="light" data-type="image"></div>';
+ $grecaptchaSelector = "div.g-recaptcha[data-sitekey=$site_key][data-theme=light][data-type=image]";
// Test if login works.
$this->drupalLogin($this->normalUser);
$this->drupalLogout();
$this->drupalGet('user/login');
- $this->assertSession()->responseNotContains($grecaptcha, '[testReCaptchaOnLoginForm]: reCAPTCHA is not shown on form.');
+ // reCAPTCHA is not shown on form.
+ $this->assertSession()->elementNotExists('css', $grecaptchaSelector);
// Enable 'captcha/Math' CAPTCHA on login form.
captcha_set_form_id_setting('user_login_form', 'captcha/Math');
$this->drupalGet('user/login');
- $this->assertSession()->responseNotContains($grecaptcha, '[testReCaptchaOnLoginForm]: reCAPTCHA is not shown on form.');
+ // reCAPTCHA is not shown on form.
+ $this->assertSession()->elementNotExists('css', $grecaptchaSelector);
// Enable 'recaptcha/reCAPTCHA' on login form.
captcha_set_form_id_setting('user_login_form', 'recaptcha/reCAPTCHA');
@@ -143,15 +145,26 @@ class ReCaptchaBasicTest extends BrowserTestBase {
// Check if there is a reCAPTCHA on the login form.
$this->drupalGet('user/login');
- $this->assertSession()->responseContains($grecaptcha, '[testReCaptchaOnLoginForm]: reCAPTCHA is shown on form.');
- $this->assertSession()->responseContains('<script src="' . Url::fromUri('https://www.google.com/recaptcha/api.js', ['query' => ['hl' => \Drupal::service('language_manager')->getCurrentLanguage()->getId()], 'absolute' => TRUE])->toString() . '" async defer></script>', '[testReCaptchaOnLoginForm]: reCAPTCHA is shown on form.');
- $this->assertSession()->responseNotContains($grecaptcha . '<noscript>', '[testReCaptchaOnLoginForm]: NoScript code is not enabled for the reCAPTCHA.');
+ // reCAPTCHA is shown on form.
+ $this->assertSession()->elementExists('css', $grecaptchaSelector);
+ $options = [
+ 'query' => [
+ 'hl' => \Drupal::service('language_manager')->getCurrentLanguage()->getId(),
+ 'render' => 'explicit',
+ 'onload' => 'drupalRecaptchaOnload',
+ ],
+ 'absolute' => TRUE,
+ ];
+ $this->assertSession()->responseContains(Html::escape(Url::fromUri('https://www.google.com/recaptcha/api.js', $options)->toString()), '[testReCaptchaOnLoginForm]: reCAPTCHA is shown on form.');
+ // NoScript code is not enabled for the reCAPTCHA.
+ $this->assertSession()->elementNotExists('css', "$grecaptchaSelector + noscript");
// Test if the fall back url is properly build and noscript code added.
$this->config('recaptcha.settings')->set('widget.noscript', 1)->save();
$this->drupalGet('user/login');
- $this->assertSession()->responseContains($grecaptcha . "\n" . '<noscript>', '[testReCaptchaOnLoginForm]: NoScript for reCAPTCHA is shown on form.');
+ // NoScript for reCAPTCHA is shown on form.
+ $this->assertSession()->elementExists('css', "$grecaptchaSelector + noscript");
$options = [
'query' => [
'k' => $site_key,
@@ -164,10 +177,25 @@ class ReCaptchaBasicTest extends BrowserTestBase {
// Check if there is a reCAPTCHA with global url on the login form.
$this->config('recaptcha.settings')->set('use_globally', TRUE)->save();
$this->drupalGet('user/login');
- $this->assertSession()->responseContains('<script src="' . Url::fromUri('https://www.recaptcha.net/recaptcha/api.js', ['query' => ['hl' => \Drupal::service('language_manager')->getCurrentLanguage()->getId()], 'absolute' => TRUE])->toString() . '" async defer></script>', '[testReCaptchaOnLoginForm]: Global reCAPTCHA is shown on form.');
+ $options = [
+ 'query' => [
+ 'hl' => \Drupal::service('language_manager')->getCurrentLanguage()->getId(),
+ 'render' => 'explicit',
+ 'onload' => 'drupalRecaptchaOnload',
+ ],
+ 'absolute' => TRUE,
+ ];
+ $this->assertSession()->responseContains(Html::escape(Url::fromUri('https://www.recaptcha.net/recaptcha/api.js', $options)->toString()), '[testReCaptchaOnLoginForm]: Global reCAPTCHA is shown on form.');
+ $options = [
+ 'query' => [
+ 'k' => $site_key,
+ 'hl' => \Drupal::service('language_manager')->getCurrentLanguage()->getId(),
+ ],
+ 'absolute' => TRUE,
+ ];
$this->assertSession()->responseContains(Html::escape(Url::fromUri('https://www.recaptcha.net/recaptcha/api/fallback', $options)->toString()), '[testReCaptchaOnLoginForm]: Global fallback URL with IFRAME has been found.');
- // Check that data-size attribute does not exists.
+ // Check that data-size attribute does not exist.
$this->config('recaptcha.settings')->set('widget.size', '')->save();
$this->drupalGet('user/login');
$element = $this->xpath('//div[@class=:class and @data-size=:size]', [':class' => 'g-recaptcha', ':size' => 'small']);
diff --git a/tests/src/Functional/RecaptchaJavascriptTest.php b/tests/src/Functional/RecaptchaJavascriptTest.php
new file mode 100644
index 0000000..4203ecb
--- /dev/null
+++ b/tests/src/Functional/RecaptchaJavascriptTest.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace Drupal\Tests\recaptcha\Functional;
+
+use Drupal\Core\Url;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Test the recaptcha module using JavaScript.
+ *
+ * @see https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha-what-should-i-do
+ *
+ * @group reCAPTCHA
+ *
+ * @dependencies recaptcha
+ */
+class RecaptchaJavascriptTest extends WebDriverTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = ['recaptcha_test'];
+
+ // These are test keys that will always validate.
+ protected const SITE_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI';
+ protected const SECRET_KEY = '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe';
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ $this->config('recaptcha.settings')
+ ->set('site_key', self::SITE_KEY)
+ ->set('secret_key', self::SECRET_KEY)
+ ->save();
+ }
+
+ /**
+ * Test the recaptcha on a form loaded via ajax that also submits via ajax.
+ */
+ public function testRecaptchaOnAJAXForm() {
+ // Load the /recaptcha-test page with the AJAX button.
+ $path = Url::fromRoute('recaptcha_test.page')->toString();
+ $this->drupalGet($path);
+
+ // No recaptcha JS on the page.
+ $this->assertSession()->responseNotContains('https://www.google.com/recaptcha/api.js', 'reCAPTCHA js is not present before the form is loaded via AJAX.');
+
+ // Click the button.
+ $this->click('a#load-ajax-form');
+
+ // Once the form is loaded
+ $this->getSession()->wait(2000, '(jQuery("form[data-drupal-selector^=recaptcha-test-ajax-form]").length > 0)');
+ $this->assertJsCondition('Drupal.behaviors.recaptcha', 100, 'recaptcha Drupal behaviors found.');
+
+ // The recaptcha should be on the page.
+ $this->assertSession()->responseContains('https://www.google.com/recaptcha/api.js', 'reCAPTCHA js has been added.');
+ $grecaptcha = $this->getSession()->getPage()->find('css', 'form .g-recaptcha');
+ $this->assertJsCondition('window.grecaptcha !== undefined', 1000, 'The Google recaptcha library is loaded.');
+ $this->assertNotEmpty($grecaptcha, 'g-recaptcha element is found.');
+
+ // Test form submission.
+ // First, try a submission that will trigger the validation error handler.
+ $this->submitForm([
+ 'email' => 'invalid@example.com',
+ ], t('Submit'));
+ $messages = $this->getMessages();
+ $this->assertNotEmpty($messages);
+ $this->assertContains('Invalid email', $messages);
+ $this->assertContains('The answer you entered for the CAPTCHA was not correct.', $messages);
+ $this->assertNotContains('Form submit successful.', $messages);
+
+ // Now submit again with a valid email.
+ $this->submitForm([
+ 'email' => 'valid@example.com',
+ ], t('Submit'));
+ $messages = $this->getMessages();
+ $this->assertContains('The answer you entered for the CAPTCHA was not correct.', $messages);
+ $this->assertNotContains('Form submit successful.', $messages);
+
+ // We need to re-validate the captcha;
+ // So click it,
+ $this->clickRecaptcha();
+ // and submit for the last time.
+ $this->submitForm([
+ 'email' => 'valid@email.com',
+ ], t('Submit'));
+ $messages = $this->getMessages();
+ $this->assertNotContains('The answer you entered for the CAPTCHA was not correct.', $messages);
+ $this->assertContains('Form submit successful.', $messages);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function submitForm(array $edit, $submit, $form_html_id = NULL) {
+ parent::submitForm($edit, $submit, $form_html_id);
+
+ // Because we're submitting the form via AJAX give it 500ms before we test
+ // anything else with the response.
+ $this->getSession()->wait(500);
+ }
+
+ /**
+ * Click the captcha checkbox element and wait for it to be validated.
+ *
+ * @param int $timeout
+ * @throws \Behat\Mink\Exception\DriverException
+ * @throws \Behat\Mink\Exception\UnsupportedDriverActionException
+ */
+ protected function clickRecaptcha($timeout = 2000) {
+ $driver = $this->getSession()->getDriver();
+ $recaptchaIFrame = $this->getSession()->getPage()->find('css', 'form .g-recaptcha iframe');
+ $driver->switchToIFrame($recaptchaIFrame->getAttribute('name'));
+ $recaptchaCheckbox = $driver->find('//span[@id="recaptcha-anchor"]');
+ if (!empty($recaptchaCheckbox)) {
+ $recaptchaCheckbox[0]->click();
+ $this->getSession()->wait($timeout, 'document.getElementById("recaptcha-anchor").attributes["aria-checked"].value === true;');
+ } else {
+ $this->fail('Unable to find recaptcha checkbox.');
+ }
+ $driver->switchToWindow();
+ $this->assertJsCondition('grecaptcha.getResponse() !== ""', $timeout, 'grecaptcha has response.');
+ }
+
+ /**
+ * Search for messages in the last html page response.
+ *
+ * @return string
+ */
+ public function getMessages($timeout = 2000) {
+ $this->getSession()->wait($timeout, '(jQuery("div.messages").length > 0)');
+ $page = $this->getSession()->getPage();
+ $messages = $page->findAll('css', 'div.messages');
+ $text = '';
+ if (isset($messages)) {
+ foreach ($messages as $message) {
+ $text .= $message->getText();
+ }
+ }
+
+ return $text;
+ }
+
+}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment