diff --git a/recaptcha.admin.inc b/recaptcha.admin.inc index 55cc5a4..607985d 100644 --- a/recaptcha.admin.inc +++ b/recaptcha.admin.inc @@ -74,10 +74,27 @@ function recaptcha_admin_settings() { '#options' => array( '' => t('Normal (default)'), 'compact' => t('Compact'), + 'invisible' => t('Invisible'), ), '#title' => t('Size'), '#type' => 'select', ); + $form['recaptcha_widget_settings']['recaptcha_badge'] = array( + '#default_value' => variable_get('recaptcha_badge', 'bottomright'), + '#description' => t('Reposition the reCAPTCHA badge. "Inline" allows you to control the CSS.'), + '#options' => array( + 'bottomright' => t('Bottom Right (default)'), + 'bottomleft' => t('Bottom Left'), + 'inline' => t('Inline'), + ), + '#title' => t('Badge'), + '#type' => 'select', + '#states' => array( + 'visible' => array( + ':input[name="recaptcha_size"]' => array('value' => 'invisible'), + ), + ), + ); $form['recaptcha_widget_settings']['recaptcha_tabindex'] = array( '#type' => 'textfield', '#title' => t('Tabindex'), diff --git a/recaptcha.invisible.js b/recaptcha.invisible.js new file mode 100644 index 0000000..5b18999 --- /dev/null +++ b/recaptcha.invisible.js @@ -0,0 +1,82 @@ +/** + * @file + * Invisible reCaptcha behaviors. + */ + +/** + * Form submit button. + * + * @type {jQuery} + */ +var $submittedFormBtn = null; + +/** + * reCaptcha data-callback that submits the form. + * + * @param token + * The validation token. + */ +function onInvisibleSubmit(token) { + // Trigger click event to submit the form. + $submittedFormBtn.click(); +} + +(function ($) { + if (!$.isFunction($.fn.addBack)) { + $.fn.addBack = function (selector) { + return this.add(selector == null ? this.prevObject : this.prevObject.filter(selector)); + } + } + + /** + * 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, settings) { + // Do addBack so we will re-attach the events if the context is the form + // itself. + $(context).find('form').addBack('form').each(function () { + var $form = $(this); + + if ($form.find('.g-recaptcha[data-size="invisible"]').length) { + var $formSubmit = $('.form-actions input[type="submit"], .form-actions button[type="submit"]', $form); + + if ($formSubmit.hasClass('ajax-processed')) { + Drupal.ajax[$formSubmit.attr('id')].options.beforeSubmit = function (form_values, element_settings, options) { + if (grecaptcha.getResponse(Drupal.behaviors.recaptcha.widgets[$('.g-recaptcha[data-size="invisible"]', $form).attr('id')])) { + return true; + } + validateInvisibleCaptcha($form); + return false; + } + } + else { + $formSubmit.click(function (e) { + if (grecaptcha.getResponse(Drupal.behaviors.recaptcha.widgets[$('.g-recaptcha[data-size="invisible"]', $form).attr('id')])) { + return; + } + e.preventDefault(); + validateInvisibleCaptcha($form); + }); + } + } + }); + + /** + * Triggers the reCaptcha to validate the form. + * + * @param {jQuery} $form + * The form object to be validated. + */ + function validateInvisibleCaptcha($form) { + $submittedFormBtn = $('input[type="submit"], button[type="submit"]', $form).not('.ignore-captcha'); + grecaptcha.execute(Drupal.behaviors.recaptcha.widgets[$('.g-recaptcha[data-size="invisible"]', $form).attr('id')]); + } + } + }; +})(jQuery); diff --git a/recaptcha.js b/recaptcha.js new file mode 100644 index 0000000..3cd64e9 --- /dev/null +++ b/recaptcha.js @@ -0,0 +1,28 @@ +/** + * @file + * Invisible reCaptcha. + */ + +(function ($, window, document, Drupal) { + Drupal.behaviors.recaptcha = { + widgets: {}, + attach: function (context, settings) { + // Return early if the reCAPTCHA script is not yet *fully* loaded. If + // not, don't worry: the onload callback drupalRecaptchaOnLoad() will + // make sure that this function is called again once grecaptcha is + // defined. + if (typeof grecaptcha == 'undefined' || typeof grecaptcha.render == 'undefined') { + return; + } + $('.g-recaptcha', context).once('drupal-recaptcha').each(function () { + Drupal.behaviors.recaptcha.widgets[this.id] = grecaptcha.render(this, $(this).data()); + }); + } + }; + + window.drupalRecaptchaOnLoad = function () { + if (typeof Drupal.behaviors.recaptcha.attach != 'undefined') { + Drupal.behaviors.recaptcha.attach(); + } + }; +})(jQuery, window, document, Drupal); diff --git a/recaptcha.module b/recaptcha.module index 8beb8f8..17fde0b 100644 --- a/recaptcha.module +++ b/recaptcha.module @@ -11,6 +11,9 @@ require_once dirname(__FILE__) . '/recaptcha-php/src/ReCaptcha/RequestParameters require_once dirname(__FILE__) . '/recaptcha-php/src/ReCaptcha/Response.php'; require_once dirname(__FILE__) . '/src/ReCaptcha/RequestMethod/Drupal7Post.php'; +use ReCaptcha\ReCaptcha; +use ReCaptcha\RequestMethod\Drupal7Post; + /** * Implements hook_help(). */ @@ -68,7 +71,7 @@ function recaptcha_permission() { /** * Implements hook_captcha(). */ -function recaptcha_captcha($op, $captcha_type = '') { +function recaptcha_captcha($op, $captcha_type = '', $captcha_sid = '') { global $language; switch ($op) { @@ -109,12 +112,13 @@ function recaptcha_captcha($op, $captcha_type = '') { $variables = array( 'sitekey' => $recaptcha_site_key, 'language' => $language->language, - 'recaptcha_src_fallback' => $recaptcha_src_fallback + 'recaptcha_src_fallback' => $recaptcha_src_fallback, ); $noscript = theme('recaptcha_widget_noscript', array('widget' => $variables)); } $attributes = array( + 'id' => 'g-recaptcha' . $captcha_sid, 'class' => 'g-recaptcha', 'data-sitekey' => $recaptcha_site_key, 'data-theme' => variable_get('recaptcha_theme', 'light'), @@ -122,26 +126,38 @@ function recaptcha_captcha($op, $captcha_type = '') { 'data-size' => variable_get('recaptcha_size', ''), 'data-tabindex' => variable_get('recaptcha_tabindex', 0), ); + + if (variable_get('recaptcha_size', '') == 'invisible') { + $attributes['data-callback'] = 'onInvisibleSubmit'; + $attributes['data-badge'] = variable_get('recaptcha_badge', 'bottomright'); + + $captcha['form']['#attached']['js'] = array( + drupal_get_path('module', 'recaptcha') . '/recaptcha.invisible.js', + ); + } // Filter out empty tabindex/size. $attributes = array_filter($attributes); $captcha['form']['recaptcha_widget'] = array( '#markup' => '', '#suffix' => $noscript, - ); - - // @todo: #1664602: D7 does not yet support "async" in drupal_add_js(). - // drupal_add_js(url('https://www.google.com/recaptcha/api.js', array('query' => array('hl' => $language->language), 'absolute' => TRUE)), array('defer' => TRUE, 'async' => TRUE, 'type' => 'external')); - $data = array( - '#tag' => 'script', - '#value' => '', - '#attributes' => array( - 'src' => url($recaptcha_src, array('query' => array('hl' => $language->language), 'absolute' => TRUE)), - 'async' => 'async', - 'defer' => 'defer', + '#attached' => array( + 'js' => array( + drupal_get_path('module', 'recaptcha') . '/recaptcha.js', + array( + 'data' => url($recaptcha_src, array( + 'query' => array( + 'hl' => $language->language, + 'onload' => 'drupalRecaptchaOnLoad', + 'render' => 'explicit', + ), + 'absolute' => TRUE, + )), + 'type' => 'external', + ), + ), ), ); - drupal_add_html_head($data, 'recaptcha_api'); } else { // Fallback to Math captcha as reCAPTCHA is not configured. @@ -162,7 +178,7 @@ function recaptcha_captcha_validation($solution, $response, $element, $form_stat } // Use drupal_http_request() to circumvent all issues with the Google library. - $recaptcha = new \ReCaptcha\ReCaptcha($recaptcha_secret_key, new \ReCaptcha\RequestMethod\Drupal7Post()); + $recaptcha = new ReCaptcha($recaptcha_secret_key, new Drupal7Post()); // Ensures the hostname matches. Required if "Domain Name Validation" is // disabled for credentials. @@ -221,7 +237,13 @@ function recaptcha_captcha_validation($solution, $response, $element, $form_stat * @see recaptcha-widget-noscript.tpl.php */ function template_preprocess_recaptcha_widget_noscript(&$variables) { - $variables['sitekey'] = check_plain($variables['widget']['sitekey']); + $variables['sitekey'] = check_plain($variables['widget']['sitekey']); $variables['language'] = check_plain($variables['widget']['language']); - $variables['url'] = check_url(url($variables['widget']['recaptcha_src_fallback'], array('query' => array('k' => $variables['widget']['sitekey'], 'hl' => $variables['widget']['language']), 'absolute' => TRUE))); + $variables['url'] = check_url(url($variables['widget']['recaptcha_src_fallback'], array( + 'query' => array( + 'k' => $variables['widget']['sitekey'], + 'hl' => $variables['widget']['language'], + ), + 'absolute' => TRUE, + ))); } diff --git a/recaptcha.test b/recaptcha.test index eeeaa57..ee801da 100644 --- a/recaptcha.test +++ b/recaptcha.test @@ -5,6 +5,9 @@ * Tests for reCAPTCHA module. */ +/** + * Helper class for ReCaptcha test cases. + */ class ReCaptchaBasicTest extends DrupalWebTestCase { /** @@ -128,20 +131,24 @@ class ReCaptchaBasicTest extends DrupalWebTestCase { // Check if there is a reCAPTCHA on the login form. $this->drupalGet('user'); - $this->assertRaw($grecaptcha, '[testReCaptchaOnLoginForm]: reCAPTCHA is shown on form.'); - $this->assertRaw('', '[testReCaptchaOnLoginForm]: reCAPTCHA is shown on form.'); + $captcha_sid = $this->getCaptchaSid(); + $grecaptcha = '
'; + $this->assertRaw($grecaptcha, '[testReCaptchaOnLoginForm]: reCAPTCHA is shown on form.' . $grecaptcha); + $this->assertRaw('', '[testReCaptchaOnLoginForm]: reCAPTCHA API script is present.'); $this->assertNoRaw($grecaptcha . '