Repository: web2py/web2py

Issue:
Book outdated - Recaptcha will close down end of March - Recaptcha2 is the way to go
The book mentions the Recaptcha **V1** API, which will shut down end of March here:
http://www.web2py.com/books/default/chapter/29/09/access-control?search=recaptcha#CAPTCHA-and-reCAPTCHA

Users need to switch to Recaptcha **V2**.

I found this web2py mailing list post, which explains all the necessary adjustments, very helpful:

https://groups.google.com/forum/#!topic/web2py/yhoxNN3HL3U

Cheers
Toby




Discussion:
Can you update the book?

We can delete the code from web2py for V1...

On Sun, Mar 25, 2018 at 3:52 PM, wish7code <notifications@github.com> wrote:

> The book mentions the Recaptcha *V1* API, which will shut down end of
> March here:
> http://www.web2py.com/books/default/chapter/29/09/access-
> control?search=recaptcha#CAPTCHA-and-reCAPTCHA
>
> Users need to switch to Recaptcha *V2*.
>
> I found this web2py mailing list post, which explains all the necessary
> adjustments, very helpful:
>
> https://groups.google.com/forum/#!topic/web2py/yhoxNN3HL3U
>
> Cheers
> Toby
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/web2py/web2py/issues/1887>, or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABlWx9nwQHtb-WngtJ0SruOS3tBlyVrZks5th_V4gaJpZM4S6Rzm>
> .
>

Monkey-patched https://github.com/web2py/web2py-book/pull/379

Resolving patch:
diff --git a/gluon/tools.py b/gluon/tools.py
--- a/gluon/tools.py
+++ b/gluon/tools.py
@@ -52,7 +52,7 @@
 Table = DAL.Table
 Field = DAL.Field
 
-__all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki',
+__all__ = ['Mail', 'Auth', 'Recaptcha2', 'Crud', 'Service', 'Wiki',
            'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate']
 
 # mind there are two loggers here (logger and crud.settings.logger)!
@@ -826,149 +826,6 @@ def encoded_or_raw(text):
         return True
 
 
-class Recaptcha(DIV):
-
-    """
-    Examples:
-        Use as::
-
-            form = FORM(Recaptcha(public_key='...', private_key='...'))
-
-        or::
-
-            form = SQLFORM(...)
-            form.append(Recaptcha(public_key='...', private_key='...'))
-
-    """
-
-    API_SSL_SERVER = 'https://www.google.com/recaptcha/api'
-    API_SERVER = 'http://www.google.com/recaptcha/api'
-    VERIFY_SERVER = 'http://www.google.com/recaptcha/api/verify'
-
-    def __init__(self,
-                 request=None,
-                 public_key='',
-                 private_key='',
-                 use_ssl=False,
-                 error=None,
-                 error_message='invalid',
-                 label='Verify:',
-                 options='',
-                 comment='',
-                 ajax=False
-                 ):
-        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.use_ssl = use_ssl
-        self.error = error
-        self.errors = Storage()
-        self.error_message = error_message
-        self.components = []
-        self.attributes = {}
-        self.label = label
-        self.options = options
-        self.comment = comment
-        self.ajax = ajax
-
-    def _validate(self):
-
-        # for local testing:
-
-        recaptcha_challenge_field = \
-            self.request_vars.recaptcha_challenge_field
-        recaptcha_response_field = \
-            self.request_vars.recaptcha_response_field
-        private_key = self.private_key
-        remoteip = self.remote_addr
-        if not (recaptcha_response_field and recaptcha_challenge_field
-                and len(recaptcha_response_field)
-                and len(recaptcha_challenge_field)):
-            self.errors['captcha'] = self.error_message
-            return False
-        params = urlencode({
-            'privatekey': private_key,
-            'remoteip': remoteip,
-            'challenge': recaptcha_challenge_field,
-            '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)
-        return_values = httpresp.read().splitlines()
-        httpresp.close()
-        return_code = return_values[0]
-        if return_code == 'true':
-            del self.request_vars.recaptcha_challenge_field
-            del self.request_vars.recaptcha_response_field
-            self.request_vars.captcha = ''
-            return True
-        else:
-            # In case we get an error code, store it so we can get an error message
-            # from the /api/challenge URL as described in the reCAPTCHA api docs.
-            self.error = return_values[1]
-            self.errors['captcha'] = self.error_message
-            return False
-
-    def xml(self):
-        public_key = self.public_key
-        use_ssl = self.use_ssl
-        error_param = ''
-        if self.error:
-            error_param = '&error=%s' % self.error
-        if use_ssl:
-            server = self.API_SSL_SERVER
-        else:
-            server = self.API_SERVER
-        if not self.ajax:
-            captcha = DIV(
-                SCRIPT("var RecaptchaOptions = {%s};" % self.options),
-                SCRIPT(_type="text/javascript",
-                       _src="%s/challenge?k=%s%s" % (server, public_key, error_param)),
-                TAG.noscript(
-                    IFRAME(
-                        _src="%s/noscript?k=%s%s" % (
-                            server, public_key, error_param),
-                        _height="300", _width="500", _frameborder="0"), BR(),
-                    INPUT(
-                        _type='hidden', _name='recaptcha_response_field',
-                        _value='manual_challenge')), _id='recaptcha')
-
-        else:  # use Google's ajax interface, needed for LOADed components
-
-            url_recaptcha_js = "%s/js/recaptcha_ajax.js" % server
-            RecaptchaOptions = "var RecaptchaOptions = {%s}" % self.options
-            script = """%(options)s;
-            jQuery.getScript('%(url)s',function() {
-                Recaptcha.create('%(public_key)s',
-                    'recaptcha',jQuery.extend(RecaptchaOptions,{'callback':Recaptcha.focus_response_field}))
-                }) """ % ({'options': RecaptchaOptions, 'url': url_recaptcha_js, 'public_key': public_key})
-            captcha = DIV(
-                SCRIPT(
-                    script,
-                    _type="text/javascript",
-                ),
-                TAG.noscript(
-                    IFRAME(
-                        _src="%s/noscript?k=%s%s" % (
-                            server, public_key, error_param),
-                        _height="300", _width="500", _frameborder="0"), BR(),
-                    INPUT(
-                        _type='hidden', _name='recaptcha_response_field',
-                        _value='manual_challenge')), _id='recaptcha')
-
-        if not self.errors.captcha:
-            return XML(captcha).xml()
-        else:
-            captcha.append(DIV(self.errors['captcha'], _class='error'))
-            return XML(captcha).xml()
-
-
 class Recaptcha2(DIV):
     """
     Experimental:


Test patch:
diff --git a/gluon/tests/test_tools.py b/gluon/tests/test_tools.py
--- a/gluon/tests/test_tools.py
+++ b/gluon/tests/test_tools.py
@@ -17,7 +17,7 @@
 from gluon.dal import DAL, Field
 from pydal.objects import Table
 from gluon import tools
-from gluon.tools import Auth, Mail, Recaptcha, Recaptcha2, prettydate, Expose
+from gluon.tools import Auth, Mail, Recaptcha2, prettydate, Expose
 from gluon._compat import PY2
 from gluon.globals import Request, Response, Session
 from gluon.storage import Storage
@@ -208,14 +208,6 @@ def test_attachment(self):
         self.assertTrue('Content-Id: <trololo>' in message.payload)
 
 
-# class TestRecaptcha(unittest.TestCase):
-#     def test_Recaptcha(self):
-#         from html import FORM
-#         form = FORM(Recaptcha(public_key='public_key', private_key='private_key'))
-#         self.assertEqual(form.xml(),
-#                          '<form action="#" enctype="multipart/form-data" method="post"><div id="recaptcha"><script><!--\nvar RecaptchaOptions = {};\n//--></script><script src="http://www.google.com/recaptcha/api/challenge?k=public_key" type="text/javascript"></script><noscript><iframe frameborder="0" height="300" src="http://www.google.com/recaptcha/api/noscript?k=public_key" width="500"></iframe><br /><input name="recaptcha_response_field" type="hidden" value="manual_challenge" /></noscript></div></form>')
-#
-#
 # class TestRecaptcha2(unittest.TestCase):
 #     def test_Recaptcha2(self):
 #         from html import FORM
@@ -248,7 +240,6 @@ def setUp(self):
                                          self.user_data['password'])[0]))
         self.jwtauth = AuthJWT(self.auth, secret_key='secret', verify_expiration=True)
 
-
     def test_jwt_token_manager(self):
         import gluon.serializers
         self.request.vars.update(self.user_data)
@@ -260,7 +251,6 @@ def test_jwt_token_manager(self):
         self.token = self.jwtauth.jwt_token_manager()
         self.assertIsNotNone(self.token)
 
-
     def test_allows_jwt(self):
         import gluon.serializers
         self.request.vars.update(self.user_data)
@@ -270,11 +260,13 @@ def test_allows_jwt(self):
         del self.request.vars['password']
         self.token = self.jwtauth.jwt_token_manager()
         self.request.vars._token = gluon.serializers.json_parser.loads(self.token)['token']
+
         @self.jwtauth.allows_jwt()
         def optional_auth():
             self.assertEqual(self.user_data['username'], self.auth.user.username)
         optional_auth()
 
+
 @unittest.skipIf(IS_IMAP, "TODO: Imap raises 'Connection refused'")
 # class TestAuth(unittest.TestCase):
 #
@@ -495,8 +487,6 @@ def optional_auth():
 #     #     impersonate_form = auth.impersonate(user_id=omer_id)
 #     #     self.assertTrue(auth.is_impersonating())
 #     #     self.assertEqual(impersonate_form, 'test')
-
-
 class TestAuth(unittest.TestCase):
 
     def myassertRaisesRegex(self, *args, **kwargs):
@@ -904,7 +894,7 @@ def test_has_membership(self):
         self.assertEqual(count_log_event_test_after, count_log_event_test_before)
 
     def test_add_membership(self):
-        user = self.db(self.db.auth_user.username == 'bart').select().first() # bypass login_bare()
+        user = self.db(self.db.auth_user.username == 'bart').select().first()  # bypass login_bare()
         user_id = user.id
         role_name = 'test_add_membership_group'
         group_id = self.auth.add_group(role_name)
@@ -1174,6 +1164,7 @@ def test_prettydate(self):
 
 pjoin = os.path.join
 
+
 def have_symlinks():
     return os.name == 'posix'
 
@@ -1181,18 +1172,20 @@ def have_symlinks():
 class Test_Expose__in_base(unittest.TestCase):
 
     def test_in_base(self):
-        are_under = [ # (sub, base)
+        are_under = [
+            # (sub, base)
             ('/foo/bar', '/foo'),
             ('/foo', '/foo'),
             ('/foo', '/'),
             ('/', '/'),
         ]
         for sub, base in are_under:
-            self.assertTrue( Expose._Expose__in_base(subdir=sub, basedir=base, sep='/'),
-                             '%s is not under %s' % (sub, base) )
+            self.assertTrue(Expose._Expose__in_base(subdir=sub, basedir=base, sep='/'),
+                            '%s is not under %s' % (sub, base))
 
     def test_not_in_base(self):
-        are_not_under = [ # (sub, base)
+        are_not_under = [
+            # (sub, base)
             ('/foobar', '/foo'),
             ('/foo', '/foo/bar'),
             ('/bar', '/foo'),
@@ -1200,8 +1193,8 @@ def test_not_in_base(self):
             ('/', '/x'),
         ]
         for sub, base in are_not_under:
-            self.assertFalse( Expose._Expose__in_base(subdir=sub, basedir=base, sep='/'),
-                              '%s should not be under %s' % (sub, base) )
+            self.assertFalse(Expose._Expose__in_base(subdir=sub, basedir=base, sep='/'),
+                             '%s should not be under %s' % (sub, base))
 
 
 class TestExpose(unittest.TestCase):
@@ -1237,7 +1230,7 @@ def tearDown(self):
         shutil.rmtree(self.base_dir)
 
     def make_dirs(self):
-        """setup direcotry strucutre"""
+        """setup directory structure"""
         for d in (['inside'],
                   ['inside', 'dir1'],
                   ['inside', 'dir2'],
@@ -1257,7 +1250,7 @@ def make_readme(self):
             f.write('README content')
 
     def make_symlinks(self):
-        """setup extenstion for posix systems"""
+        """setup extension for posix systems"""
         # inside links
         os.symlink(
             pjoin(self.base_dir, 'inside', 'dir1'),