Page Menu
Home
GRNET
Search
Configure Global Search
Log In
Files
F1614877
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Sat, Mar 21, 10:15 AM
Size
32 KB
Mime Type
text/x-diff
Expires
Mon, Mar 23, 10:15 AM (19 h, 16 m)
Engine
blob
Format
Raw Data
Handle
353384
Attached To
rDJANGONOCAPTCHA django-nocaptcha-recaptcha
View Options
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..508a30c
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,5 @@
+Contributors based on gitlog:
+Joe Jasinski
+Jin Sun
+amatellanes
+Pablo Recio (pablorecio)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ebc8d00
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,31 @@
+Copyright 2014 (c) Imaginary Landscape LLC
+All rights reserved.
+
+Major updates to the project and name change made by Imaginary Landscape LLC.
+All licensing follows the below 3-clause BSD license.
+
+
+Copyright (c) Praekelt Foundation
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Praekelt Foundation nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL PRAEKELT FOUNDATION BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..15f1435
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,9 @@
+global-include LICENSE README.md setup.py LICENSE MANIFEST.in
+include LICENSE
+include README.md
+include MANIFEST.in
+include setup.py
+recursive-include demo *
+recursive-include nocaptcha_recaptcha *
+recursive-exclude * __pycache__
+recursive-exclude * *.py[co]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0d02c03
--- /dev/null
+++ b/README.md
@@ -0,0 +1,97 @@
+[](https://travis-ci.org/ImaginaryLandscape/django-nocaptcha-recaptcha)
+
+# SUMMARY
+
+Add new-style Google ReCaptcha widgets to your Django forms simply by adding a
+NoReCaptchaField field to said forms.
+
+# ABOUT
+
+In late 2014, Google updated their ReCaptcha service, changing its API. The update significantly
+changes the appearance and function of ReCaptcha. This has been referred to as
+ReCaptcha 2 or "nocaptcha recaptcha".
+
+This module is intended to be a successor to django-recaptcha to support the new style
+Google Recaptcha. It borrows a lot of the logic from the django-recaptcha, but has been
+updated to support the Google change.
+
+For the Google documentation for this service, visit the following:
+
+ https://developers.google.com/recaptcha/intro
+
+The original django-recaptcha project is located at the following location:
+
+ https://github.com/praekelt/django-recaptcha
+
+# FEATURES
+
+ - Implements Google's New "NoCaptcha ReCaptcha Field"
+ - Uses the fallback option for browsers without JavaScript
+ - Easy to add to a Form via a FormField
+ - Works similar to django-recaptcha
+ - Working demo projects
+ - Works with Python 2.7 and 3.4
+
+# INSTALL
+
+ pip install django-nocaptcha-recaptcha
+
+# CONFIGURE
+
+Add nocaptcha_recaptcha to your INSTALLED_APPS setting
+
+Add the following to settings.py
+
+ Required settings:
+ NORECAPTCHA_SITE_KEY (string) = the Google provided site_key
+ NORECAPTCHA_SECRET_KEY (string) = the Google provided secret_key
+
+ Optional Settings:
+ NORECAPTCHA_VERIFY_URL (string) = reCaptcha api endpoint for verification.
+ Best to leave this as the default setting.
+ Default is https://www.google.com/recaptcha/api/siteverify
+ NORECAPTCHA_WIDGET_TEMPLATE (string) = location for the widget template.
+ Default is nocaptcha_recaptcha/widget.html
+
+
+Add the field to a form that you want to protect.
+
+ from nocaptcha_recaptcha.fields import NoReCaptchaField
+
+ class DemoForm(forms.Form):
+ .....
+ captcha = NoReCaptchaField()
+
+
+Add Google's JavaScript library to your base template or elsewhere, so it is
+available on the page containing the django form.
+
+ <script src="https://www.google.com/recaptcha/api.js" async defer></script>
+
+
+(optional)
+You can customize the field.
+
+- You can add attributes to the g-recaptcha div tag through the following
+
+ captcha = NoReCaptchaField(gtag_attrs={'data-theme':'dark'}))
+
+- You can override the template for the widget like you would any
+ other django template.
+
+
+# DEMO PROJECT
+
+The demo project includes a fully working example of this module.
+To use it, run the following:
+
+ cd demo
+ export NORECAPTCHA_SITE_KEY="<your site key>"
+ export NORECAPTCHA_SECRET_KEY="<your secret key>"
+ ./manage.py runserver
+
+ # in a browser, visit http://localhost:8000
+
+# TESTING
+
+ python setup.py test
diff --git a/demo/demo/__init__.py b/demo/demo/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/demo/demo/settings.py b/demo/demo/settings.py
new file mode 100644
index 0000000..d763a55
--- /dev/null
+++ b/demo/demo/settings.py
@@ -0,0 +1,212 @@
+# Django settings for demo project.
+import os
+import sys
+from django import VERSION
+
+PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ))
+sys.path.insert(0, os.path.join(PROJECT_ROOT, '..', '..'))
+
+DEBUG = True
+
+ADMINS = (
+ # ('Your Name', 'your_email@example.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+ 'default': {
+ # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': 'demo.sqlite3', # Or path to database file if using sqlite3.
+ # The following settings are not used with sqlite3:
+ 'USER': '',
+ 'PASSWORD': '',
+ 'HOST': '',
+ # Empty for localhost through domain sockets or '127.0.0.1' for
+ # localhost through TCP.
+ 'PORT': '', # Set to empty string for default.
+ }
+}
+
+
+# Hosts/domain names that are valid for this site; required if DEBUG is False
+# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
+ALLOWED_HOSTS = []
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# In a Windows environment this must be set to your system time zone.
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale.
+USE_L10N = True
+
+# If you set this to False, Django will not use timezone-aware datetimes.
+USE_TZ = True
+
+# Absolute filesystem path to the directory that will hold user-uploaded files.
+# Example: "/var/www/example.com/media/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash.
+# Examples: "http://example.com/media/", "http://media.example.com/"
+MEDIA_URL = ''
+
+# Absolute path to the directory static files should be collected to.
+# Don't put anything in this directory yourself; store your static files
+# in apps' "static/" subdirectories and in STATICFILES_DIRS.
+# Example: "/var/www/example.com/static/"
+STATIC_ROOT = ''
+
+# URL prefix for static files.
+# Example: "http://example.com/static/", "http://static.example.com/"
+STATIC_URL = '/static/'
+
+# Additional locations of static files
+STATICFILES_DIRS = (
+ # Put strings here, like "/home/html/static" or "C:/www/django/static".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+)
+
+# List of finder classes that know how to find static files in
+# various locations.
+STATICFILES_FINDERS = (
+ 'django.contrib.staticfiles.finders.FileSystemFinder',
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+ # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
+)
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'x$47^-hmv-kaa0trcc*ry%+b^^2f)$rs#cl)6j&!)j2c&h%88e'
+
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ # Uncomment the next line for simple clickjacking protection:
+ # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+)
+
+ROOT_URLCONF = 'demo.urls'
+
+# Python dotted path to the WSGI application used by Django's runserver.
+WSGI_APPLICATION = 'demo.wsgi.application'
+
+
+if VERSION >= (1, 8, 0):
+
+ TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [
+ os.path.join(PROJECT_ROOT, 'templates'),
+ ],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.contrib.auth.context_processors.auth',
+ 'django.template.context_processors.i18n',
+ 'django.template.context_processors.request',
+ 'django.template.context_processors.media',
+ 'django.template.context_processors.static',
+ 'django.contrib.messages.context_processors.messages',
+ 'example.apps.core.context_processors.site',
+ ],
+ },
+ },
+ ]
+else:
+
+ TEMPLATE_DEBUG = DEBUG
+
+ # List of callables that know how to import templates from various sources.
+ TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+ # 'django.template.loaders.eggs.Loader',
+ )
+
+ TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+ os.path.join(PROJECT_ROOT, 'templates'),
+ )
+
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'django.contrib.admin',
+ # Uncomment the next line to enable admin documentation:
+ # 'django.contrib.admindocs',
+
+ # Only needed for running unit tests
+ 'nocaptcha_recaptcha',
+)
+
+
+SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'
+
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error when DEBUG=False.
+# See http://docs.djangoproject.com/en/dev/topics/logging for
+# more details on how to customize your logging configuration.
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'filters': {
+ 'require_debug_false': {
+ '()': 'django.utils.log.RequireDebugFalse'
+ }
+ },
+ 'handlers': {
+ 'mail_admins': {
+ 'level': 'ERROR',
+ 'filters': ['require_debug_false'],
+ 'class': 'django.utils.log.AdminEmailHandler'
+ },
+ 'console': {
+ 'level': 'DEBUG',
+ 'class': 'logging.StreamHandler',
+ },
+ },
+ 'loggers': {
+ 'django.request': {
+ 'handlers': ['mail_admins'],
+ 'level': 'ERROR',
+ 'propagate': True,
+ },
+ 'iscapeauth': {
+ 'handlers': ['console'],
+ 'level': "DEBUG",
+ 'propogate': True,
+ }
+ }
+}
+
+NORECAPTCHA_SITE_KEY = os.environ.get('NORECAPTCHA_SITE_KEY', "")
+NORECAPTCHA_SECRET_KEY = os.environ.get('NORECAPTCHA_SECRET_KEY', "")
diff --git a/demo/demo/templates/index.html b/demo/demo/templates/index.html
new file mode 100644
index 0000000..e02b2b4
--- /dev/null
+++ b/demo/demo/templates/index.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+ <title>reCAPTCHA demo: Simple page</title>
+ <script src="https://www.google.com/recaptcha/api.js" async defer></script>
+</head>
+<body>
+
+<form method="post" action=".">{% csrf_token %}
+ {{ form.as_p }}
+ <button type="submit">Submit</button>
+</form>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/demo/demo/templates/success.html b/demo/demo/templates/success.html
new file mode 100644
index 0000000..7af069f
--- /dev/null
+++ b/demo/demo/templates/success.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title>reCAPTCHA demo: Simple page</title>
+</head>
+<body>
+Success!
+</body>
+</html>
\ No newline at end of file
diff --git a/demo/demo/urls.py b/demo/demo/urls.py
new file mode 100644
index 0000000..0307794
--- /dev/null
+++ b/demo/demo/urls.py
@@ -0,0 +1,18 @@
+from django.contrib import admin
+from django.conf.urls import include, url
+from django.views.generic import TemplateView
+
+from . import views
+
+admin.autodiscover()
+
+urlpatterns = [
+
+ url(r'^$', views.DemoView.as_view(template_name="index.html"), {},
+ name="index"),
+ url(r'^success/$', TemplateView.as_view(template_name="success.html"), {},
+ name="success"),
+
+ # Uncomment the next line to enable the admin:
+ url(r'^admin/', include(admin.site.urls)),
+]
diff --git a/demo/demo/views.py b/demo/demo/views.py
new file mode 100644
index 0000000..6d2c4b4
--- /dev/null
+++ b/demo/demo/views.py
@@ -0,0 +1,18 @@
+from django import forms
+from django.core.urlresolvers import reverse_lazy
+from django.views.generic import FormView
+
+from nocaptcha_recaptcha.fields import NoReCaptchaField
+
+
+class DemoForm(forms.Form):
+ username = forms.CharField(required=True)
+ captcha = NoReCaptchaField(gtag_attrs={'data-theme': 'dark'})
+
+
+class DemoView(FormView):
+ form_class = DemoForm
+ success_url = reverse_lazy('success')
+
+ def form_valid(self, form):
+ return super(DemoView, self).form_valid(form)
\ No newline at end of file
diff --git a/demo/demo/wsgi.py b/demo/demo/wsgi.py
new file mode 100644
index 0000000..8d4d998
--- /dev/null
+++ b/demo/demo/wsgi.py
@@ -0,0 +1,33 @@
+"""
+WSGI config for demo project.
+
+This module contains the WSGI application used by Django's development server
+and any production WSGI deployments. It should expose a module-level variable
+named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
+this application via the ``WSGI_APPLICATION`` setting.
+
+Usually you will have the standard Django WSGI application here, but it also
+might make sense to replace the whole Django WSGI application with a custom one
+that later delegates to the Django one. For example, you could introduce WSGI
+middleware here, or combine a Django application with an application of another
+framework.
+
+"""
+import os
+
+# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
+# if running multiple sites in the same mod_wsgi process. To fix this, use
+# mod_wsgi daemon mode with each site in its own daemon process, or use
+# os.environ["DJANGO_SETTINGS_MODULE"] = "demo.settings"
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings")
+
+# This application object is used by any WSGI server configured to use this
+# file. This includes Django's development server, if the WSGI_APPLICATION
+# setting points here.
+from django.core.wsgi import get_wsgi_application
+
+application = get_wsgi_application()
+
+# Apply WSGI middleware here.
+# from helloworld.wsgi import HelloWorldApplication
+# application = HelloWorldApplication(application)
diff --git a/demo/manage.py b/demo/manage.py
new file mode 100755
index 0000000..86cc0b0
--- /dev/null
+++ b/demo/manage.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/demo/requirements.txt b/demo/requirements.txt
new file mode 100644
index 0000000..b16808d
--- /dev/null
+++ b/demo/requirements.txt
@@ -0,0 +1,3 @@
+Django>=1.5,<1.11
+mock>=1.0.1
+six>=1.8.0
diff --git a/nocaptcha_recaptcha/__init__.py b/nocaptcha_recaptcha/__init__.py
new file mode 100644
index 0000000..50404c7
--- /dev/null
+++ b/nocaptcha_recaptcha/__init__.py
@@ -0,0 +1,4 @@
+__version__ = '0.0.19'
+
+from .fields import NoReCaptchaField
+from .widgets import NoReCaptchaWidget
diff --git a/nocaptcha_recaptcha/_compat.py b/nocaptcha_recaptcha/_compat.py
new file mode 100644
index 0000000..22c1f91
--- /dev/null
+++ b/nocaptcha_recaptcha/_compat.py
@@ -0,0 +1,18 @@
+import sys
+
+PY2 = sys.version_info[0] == 2
+if PY2:
+ text_type = unicode
+ from urllib2 import Request, urlopen
+ from urllib import urlencode
+else:
+ from urllib.request import Request, urlopen
+ from urllib.parse import urlencode
+
+ text_type = str
+
+
+def want_bytes(s, encoding='utf-8', errors='strict'):
+ if isinstance(s, text_type):
+ s = s.encode(encoding, errors)
+ return s
diff --git a/nocaptcha_recaptcha/client.py b/nocaptcha_recaptcha/client.py
new file mode 100644
index 0000000..84dcadd
--- /dev/null
+++ b/nocaptcha_recaptcha/client.py
@@ -0,0 +1,117 @@
+import logging
+
+import django
+
+try:
+ import json
+except ImportError:
+ from django.utils import simplejson as json
+
+from django.conf import settings
+from django.template.loader import render_to_string
+from django.utils.translation import get_language
+from django.utils.encoding import force_text
+
+from ._compat import want_bytes, urlencode, Request, urlopen, PY2
+
+logger = logging.getLogger(__name__)
+
+DEFAULT_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
+DEFAULT_FALLBACK_URL = "https://www.google.com/recaptcha/api/fallback"
+DEFAULT_WIDGET_TEMPLATE = 'nocaptcha_recaptcha/widget.html'
+
+VERIFY_URL = getattr(settings, "NORECAPTCHA_VERIFY_URL",
+ DEFAULT_VERIFY_URL)
+
+FALLBACK_URL = getattr(settings, "NORECAPTCHA_FALLBACK_URL",
+ DEFAULT_FALLBACK_URL)
+
+WIDGET_TEMPLATE = getattr(settings, "NORECAPTCHA_WIDGET_TEMPLATE",
+ DEFAULT_WIDGET_TEMPLATE)
+
+
+class RecaptchaResponse(object):
+ def __init__(self, is_valid, error_codes=None):
+ self.is_valid = is_valid
+ self.error_codes = error_codes
+
+
+def displayhtml(site_key, gtag_attrs, js_params):
+ """Gets the HTML to display for reCAPTCHA
+
+ site_key -- The public api key provided by Google ReCaptcha
+ """
+
+ if 'hl' not in js_params:
+ js_params['hl'] = get_language()[:2]
+
+ return render_to_string(
+ WIDGET_TEMPLATE,
+ {
+ 'fallback_url': FALLBACK_URL,
+ 'site_key': site_key,
+ 'js_params': js_params,
+ 'gtag_attrs': gtag_attrs,
+ })
+
+
+def submit(g_nocaptcha_response_value, secret_key, remoteip):
+ """
+ Submits a reCAPTCHA request for verification. Returns RecaptchaResponse
+ for the request
+
+ recaptcha_response_field -- The value of recaptcha_response_field
+ from the form
+ secret_key -- your reCAPTCHA private key
+ remoteip -- the user's ip address
+ """
+
+ if not (g_nocaptcha_response_value and len(g_nocaptcha_response_value)):
+ return RecaptchaResponse(
+ is_valid=False,
+ error_codes=['incorrect-captcha-sol']
+ )
+
+ params = urlencode({
+ 'secret': want_bytes(secret_key),
+ 'remoteip': want_bytes(remoteip),
+ 'response': want_bytes(g_nocaptcha_response_value),
+ })
+
+ if not PY2:
+ params = params.encode('utf-8')
+
+ req = Request(
+ url=VERIFY_URL, data=params,
+ headers={
+ 'Content-type': 'application/x-www-form-urlencoded',
+ 'User-agent': 'noReCAPTCHA Python'
+ }
+ )
+
+ httpresp = urlopen(req)
+
+ try:
+ res = force_text(httpresp.read())
+ return_values = json.loads(res)
+ except (ValueError, TypeError):
+ return RecaptchaResponse(
+ is_valid=False,
+ error_codes=['json-read-issue']
+ )
+ except:
+ return RecaptchaResponse(
+ is_valid=False,
+ error_codes=['unknown-network-issue']
+ )
+ finally:
+ httpresp.close()
+
+ return_code = return_values.get("success", False)
+ error_codes = return_values.get('error-codes', [])
+ logger.debug("%s - %s" % (return_code, error_codes))
+
+ if return_code is True:
+ return RecaptchaResponse(is_valid=True)
+ else:
+ return RecaptchaResponse(is_valid=False, error_codes=error_codes)
diff --git a/nocaptcha_recaptcha/fields.py b/nocaptcha_recaptcha/fields.py
new file mode 100644
index 0000000..738a295
--- /dev/null
+++ b/nocaptcha_recaptcha/fields.py
@@ -0,0 +1,75 @@
+import os
+import sys
+
+from django import forms
+from django.conf import settings
+
+try:
+ from django.utils.encoding import smart_unicode
+except ImportError:
+ from django.utils.encoding import smart_text as smart_unicode
+
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+
+from . import client
+from .widgets import NoReCaptchaWidget
+
+
+class NoReCaptchaField(forms.CharField):
+ default_error_messages = {
+ 'captcha_invalid': _('Incorrect, please try again.')
+ }
+
+ def __init__(self, site_key=None, secret_key=None,
+ gtag_attrs={}, js_params={}, *args, **kwargs):
+ """
+ site_key = the Google provided site_key
+ secret_key = the Google provided secret_key
+ gtag_attrs = html input attributes to provide
+ to the g-recaptcha tag
+ js_params = parameters to passed to the javascript backend
+
+ See: https://developers.google.com/recaptcha/docs/display
+ """
+ site_key = site_key if site_key else \
+ settings.NORECAPTCHA_SITE_KEY
+ self.secret_key = secret_key if secret_key else \
+ settings.NORECAPTCHA_SECRET_KEY
+
+ self.widget = NoReCaptchaWidget(
+ site_key=site_key, gtag_attrs=gtag_attrs, js_params=js_params)
+ self.required = True
+ super(NoReCaptchaField, self).__init__(*args, **kwargs)
+
+ def get_remote_ip(self):
+ """
+ Return the remote IP from the request.
+ First check the REMOTE_ADDR header and then the
+ HTTP_X_FORWARDED_FOR header.
+ """
+ f = sys._getframe()
+ while f:
+ if 'request' in f.f_locals:
+ request = f.f_locals['request']
+ if request:
+ remote_ip = request.META.get('REMOTE_ADDR', '')
+ forwarded_ip = request.META.get('HTTP_X_FORWARDED_FOR', '')
+ ip = remote_ip if not forwarded_ip else forwarded_ip
+ return ip
+ f = f.f_back
+
+ def clean(self, value):
+ super(NoReCaptchaField, self).clean(value)
+ g_nocaptcha_response_value = smart_unicode(value)
+ if os.environ.get('NORECAPTCHA_TESTING', None) == 'True' \
+ and g_nocaptcha_response_value == 'PASSED':
+ return value
+
+ check_captcha = client.submit(
+ g_nocaptcha_response_value, secret_key=self.secret_key,
+ remoteip=self.get_remote_ip())
+
+ if not check_captcha.is_valid:
+ raise ValidationError(self.error_messages['captcha_invalid'])
+ return value
diff --git a/nocaptcha_recaptcha/models.py b/nocaptcha_recaptcha/models.py
new file mode 100644
index 0000000..e69de29
diff --git a/nocaptcha_recaptcha/templates/nocaptcha_recaptcha/widget.html b/nocaptcha_recaptcha/templates/nocaptcha_recaptcha/widget.html
new file mode 100644
index 0000000..17d6d11
--- /dev/null
+++ b/nocaptcha_recaptcha/templates/nocaptcha_recaptcha/widget.html
@@ -0,0 +1,21 @@
+<div class="g-recaptcha" {% for attr in gtag_attrs.items %}{{ attr.0 }}="{{ attr.1 }}" {% endfor %}data-sitekey="{{ site_key }}"></div>
+<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="{{ fallback_url }}?k={{ site_key }}"
+ 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>
\ No newline at end of file
diff --git a/nocaptcha_recaptcha/tests.py b/nocaptcha_recaptcha/tests.py
new file mode 100644
index 0000000..9b0205c
--- /dev/null
+++ b/nocaptcha_recaptcha/tests.py
@@ -0,0 +1,79 @@
+import os
+import json
+
+from django.forms import Form
+from django.test import TestCase
+
+import mock
+
+from nocaptcha_recaptcha import fields, client
+
+
+class TestForm(Form):
+ captcha = fields.NoReCaptchaField(gtag_attrs={'data-theme': 'dark'})
+
+
+class TestCase(TestCase):
+ def setUp(self):
+ os.environ['NORECAPTCHA_TESTING'] = 'True'
+
+ def test_envvar_enabled(self):
+ form_params = {'g-recaptcha-response': 'PASSED'}
+ form = TestForm(form_params)
+ self.assertTrue(form.is_valid())
+
+ def test_envvar_disabled(self):
+ os.environ['NORECAPTCHA_TESTING'] = 'False'
+ form_params = {'g-recaptcha-response': 'PASSED'}
+ form = TestForm(form_params)
+ self.assertFalse(form.is_valid())
+
+ @mock.patch('nocaptcha_recaptcha.client.urlopen')
+ def test_client_submit_empty_input(self, mock_urlopen):
+ """
+ Should return False if input is empty string
+ """
+ result = client.submit('', '', '')
+ self.assertFalse(result.is_valid)
+ self.assertEqual(['incorrect-captcha-sol'], result.error_codes)
+
+ @mock.patch('nocaptcha_recaptcha.client.urlopen')
+ def test_client_submit_correct(self, mock_urlopen):
+ """
+ Should return True if response is correct
+ """
+ mock_resp = mock.Mock()
+ mock_resp.read.return_value = json.dumps(
+ {'success': True, 'error-codes': []})
+ mock_urlopen.return_value = mock_resp
+ result = client.submit('a', 'a', 'a')
+ self.assertTrue(result.is_valid)
+ self.assertEqual(result.error_codes, None)
+
+ @mock.patch('nocaptcha_recaptcha.client.urlopen')
+ def test_client_submit_response_not_json(self, mock_urlopen):
+ """
+ Should return json read error if response is not json
+ """
+ mock_resp = mock.Mock()
+ mock_resp.read.return_value = "{'success': True, 'error-codes': []}"
+ mock_urlopen.return_value = mock_resp
+ result = client.submit('a', 'a', 'a')
+ self.assertFalse(result.is_valid)
+ self.assertEqual(result.error_codes, ['json-read-issue'])
+
+ @mock.patch('nocaptcha_recaptcha.client.urlopen')
+ def test_client_submit_response_incorrect(self, mock_urlopen):
+ """
+ Should return false if response is incorrect
+ """
+ mock_resp = mock.Mock()
+ mock_resp.read.return_value = json.dumps(
+ {'success': False, 'error-codes': ['ERROR']})
+ mock_urlopen.return_value = mock_resp
+ result = client.submit('a', 'a', 'a')
+ self.assertFalse(result.is_valid)
+ self.assertEqual(result.error_codes, ['ERROR'])
+
+ def tearDown(self):
+ del os.environ['NORECAPTCHA_TESTING']
diff --git a/nocaptcha_recaptcha/views.py b/nocaptcha_recaptcha/views.py
new file mode 100644
index 0000000..e69de29
diff --git a/nocaptcha_recaptcha/widgets.py b/nocaptcha_recaptcha/widgets.py
new file mode 100644
index 0000000..95380e5
--- /dev/null
+++ b/nocaptcha_recaptcha/widgets.py
@@ -0,0 +1,24 @@
+from django import forms
+from django.conf import settings
+from django.utils.safestring import mark_safe
+
+from . import client
+
+
+class NoReCaptchaWidget(forms.widgets.Widget):
+ g_nocaptcha_response = 'g-recaptcha-response'
+
+ def __init__(self, site_key=None,
+ gtag_attrs={}, js_params={}, *args, **kwargs):
+ self.site_key = site_key if site_key else \
+ settings.NORECAPTCHA_SITE_KEY
+ super(NoReCaptchaWidget, self).__init__(*args, **kwargs)
+ self.gtag_attrs = gtag_attrs
+ self.js_params = js_params
+
+ def render(self, name, value, gtag_attrs=None, **kwargs):
+ return mark_safe(u'%s' % client.displayhtml(
+ self.site_key, self.gtag_attrs, self.js_params))
+
+ def value_from_datadict(self, data, files, name):
+ return data.get(self.g_nocaptcha_response, None)
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..40c19d7
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,38 @@
+import os
+from setuptools import setup, find_packages
+
+
+def read(fname):
+ return open(os.path.join(os.path.dirname(__file__), fname)).read()
+
+
+setup(
+ name='django-nocaptcha-recaptcha',
+ version='0.0.20',
+ description='Django nocaptcha recaptcha form field/widget app.',
+ long_description=read('README.md'),
+ author='Imaginary Landscape',
+ author_email='jjasinski@imgescape.com',
+ keywords=['django', 'recaptcha', 'field', 'nocaptcha'],
+ license='BSD',
+ url='https://github.com/ImaginaryLandscape/django-nocaptcha-recaptcha',
+ packages=find_packages(),
+ tests_require=[
+ 'mock',
+ ],
+ test_suite="setuptest.setuptest.SetupTestSuite",
+ include_package_data=True,
+ classifiers=[
+ "Framework :: Django",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: OS Independent",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python :: 2.6",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
+ "Programming Language :: Python :: 3.6",
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
+ ],
+ zip_safe=False,
+)
diff --git a/test_settings.py b/test_settings.py
new file mode 100644
index 0000000..4ff3137
--- /dev/null
+++ b/test_settings.py
@@ -0,0 +1,23 @@
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': 'test.sqlite',
+ }
+}
+
+INSTALLED_APPS = [
+ 'nocaptcha_recaptcha',
+]
+
+MIDDLEWARE_CLASSES = [
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.locale.LocaleMiddleware',
+ 'django.middleware.doc.XViewMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+]
+
+NORECAPTCHA_SECRET_KEY = 'privkey'
+NORECAPTCHA_SITE_KEY = 'pubkey'
Event Timeline
Log In to Comment