Repository: web2py/web2py

Issue:
Support the new reCaptchas
Are there any plans to support the new "I'm not a robot" reCaptchas out of the box? I know the old styled recaptchas are currently supported, but it would be better to use the newest.

https://developers.google.com/recaptcha/



Discussion:

Resolving patch:
diff --git a/gluon/tools.py b/gluon/tools.py
--- a/gluon/tools.py
+++ b/gluon/tools.py
@@ -60,7 +60,7 @@
         # fallback to pure-Python module
         import gluon.contrib.simplejson as json_parser
 
-__all__ = ['Mail', 'Auth', 'Recaptcha', 'Crud', 'Service', 'Wiki',
+__all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki',
            'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate']
 
 ### mind there are two loggers here (logger and crud.settings.logger)!
@@ -965,7 +965,142 @@ def xml(self):
             return XML(captcha).xml()
 
 
-# this should only be used for catcha and perhaps not even for that
+class Recaptcha2(DIV):
+    """
+    Experimental:
+    Creates a DIV holding the newer Recaptcha from Google (v2)
+
+    Args:
+        request : the request. If not passed, uses current request
+        public_key : the public key Google gave you
+        private_key : the private key Google gave you
+        error_message : the error message to show if verification fails
+        label : the label to use
+        options (dict) : takes these parameters
+
+            - hl
+            - theme
+            - type
+            - tabindex
+            - callback
+            - expired-callback
+
+            see https://developers.google.com/recaptcha/docs/display for docs about those
+
+        comment : the comment
+
+    Examples:
+        Use as::
+
+            form = FORM(Recaptcha2(public_key='...',private_key='...'))
+
+        or::
+
+            form = SQLFORM(...)
+            form.append(Recaptcha2(public_key='...',private_key='...'))
+
+        to protect the login page instead, use::
+
+            from gluon.tools import Recaptcha2
+            auth.settings.captcha = Recaptcha2(request, public_key='...',private_key='...')
+
+    """
+
+    API_URI = 'https://www.google.com/recaptcha/api.js'
+    VERIFY_SERVER = 'https://www.google.com/recaptcha/api/siteverify'
+
+    def __init__(self,
+                 request=None,
+                 public_key='',
+                 private_key='',
+                 error_message='invalid',
+                 label='Verify:',
+                 options=None,
+                 comment='',
+                 ):
+        request = request or current.request
+        self.request_vars = request and request.vars or current.request.vars
+        self.remote_addr = request.env.remote_addr
+        self.public_key = public_key
+        self.private_key = private_key
+        self.errors = Storage()
+        self.error_message = error_message
+        self.components = []
+        self.attributes = {}
+        self.label = label
+        self.options = options or {}
+        self.comment = comment
+
+    def _validate(self):
+        recaptcha_response_field = self.request_vars.pop('g-recaptcha-response', None)
+        remoteip = self.remote_addr
+        if not recaptcha_response_field:
+            self.errors['captcha'] = self.error_message
+            return False
+        params = urllib.urlencode({
+            'secret': self.private_key,
+            'remoteip': remoteip,
+            'response': recaptcha_response_field,
+        })
+        request = urllib2.Request(
+            url=self.VERIFY_SERVER,
+            data=params,
+            headers={'Content-type': 'application/x-www-form-urlencoded',
+                     'User-agent': 'reCAPTCHA Python'})
+        httpresp = urllib2.urlopen(request)
+        content = httpresp.read()
+        httpresp.close()
+        try:
+            response_dict = json_parser.loads(content)
+        except:
+            self.errors['captcha'] = self.error_message
+            return False
+        if response_dict.get('success', False):
+            self.request_vars.captcha = ''
+            return True
+        else:
+            self.errors['captcha'] = self.error_message
+            return False
+
+    def xml(self):
+        api_uri = self.API_URI
+        hl = self.options.pop('hl', None)
+        if hl:
+            api_uri = self.API_URI + '?hl=%s' % hl
+        public_key = self.public_key
+        self.options['sitekey'] = public_key
+        captcha = DIV(
+            SCRIPT(_src=api_uri, _async='', _defer=''),
+            DIV(_class="g-recaptcha", data=self.options),
+            TAG.noscript(XML("""
+<div style="width: 302px; height: 352px;">
+<div style="width: 302px; height: 352px; position: relative;">
+  <div style="width: 302px; height: 352px; position: absolute;">
+    <iframe src="https://www.google.com/recaptcha/api/fallback?k=%(public_key)s"
+            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>""" % dict(public_key=public_key))
+            )
+        )
+        if not self.errors.captcha:
+            return XML(captcha).xml()
+        else:
+            captcha.append(DIV(self.errors['captcha'], _class='error'))
+            return XML(captcha).xml()
+
+
+# this should only be used for captcha and perhaps not even for that
 def addrow(form, a, b, c, style, _id, position=-1):
     if style == "divs":
         form[0].insert(position, DIV(DIV(LABEL(a), _class='w2p_fl'),
@@ -987,6 +1122,15 @@ def addrow(form, a, b, c, style, _id, position=-1):
                                      DIV(b, SPAN(c, _class='inline-help'),
                                          _class='controls'),
                                      _class='control-group', _id=_id))
+    elif style == "bootstrap3_inline":
+        form[0].insert(position, DIV(LABEL(a, _class='control-label col-sm-3'),
+                                     DIV(b, SPAN(c, _class='help-block'),
+                                         _class='col-sm-9'),
+                                     _class='form-group', _id=_id))
+    elif style == "bootstrap3_stacked":
+        form[0].insert(position, DIV(LABEL(a, _class='control-label'),
+                                     b, SPAN(c, _class='help-block'),
+                                     _class='form-group', _id=_id))
     else:
         form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'),
                                     TD(b, _class='w2p_fw'),
@@ -1330,8 +1474,7 @@ def __init__(self, environment=None, db=None, mailer=True,
             logged_url=URL(controller, function, args='profile'),
             download_url=URL(controller, 'download'),
             mailer=(mailer is True) and Mail() or mailer,
-            on_failed_authorization =
-            URL(controller, function, args='not_authorized'),
+            on_failed_authorization = URL(controller, function, args='not_authorized'),
             login_next = url_index,
             login_onvalidation = [],
             login_onaccept = [],


Test patch: