1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-03 16:10:26 +00:00

Added captcha to thirdpart

This commit is contained in:
zhengxie
2013-11-09 10:42:06 +08:00
parent e6867f56f2
commit a611d11088
30 changed files with 1402 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
import re
VERSION = (0, 4, 0)
def get_version(svn=False):
"Returns the version as a human-format string."
return '.'.join([str(i) for i in VERSION])
def pillow_required():
def pil_version(version):
try:
return int(re.compile('[^\d]').sub('', version))
except:
return 116
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
try:
import Image
import ImageDraw
import ImageFont
except ImportError:
return True
return pil_version(Image.VERSION) < 116

View File

View File

@@ -0,0 +1,53 @@
import os
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
CAPTCHA_FONT_PATH = getattr(settings, 'CAPTCHA_FONT_PATH', os.path.normpath(os.path.join(os.path.dirname(__file__), '..', 'fonts/Vera.ttf')))
CAPTCHA_FONT_SIZE = getattr(settings, 'CAPTCHA_FONT_SIZE', 22)
CAPTCHA_LETTER_ROTATION = getattr(settings, 'CAPTCHA_LETTER_ROTATION', (-35, 35))
CAPTCHA_BACKGROUND_COLOR = getattr(settings, 'CAPTCHA_BACKGROUND_COLOR', '#ffffff')
CAPTCHA_FOREGROUND_COLOR = getattr(settings, 'CAPTCHA_FOREGROUND_COLOR', '#001100')
CAPTCHA_CHALLENGE_FUNCT = getattr(settings, 'CAPTCHA_CHALLENGE_FUNCT', 'captcha.helpers.random_char_challenge')
CAPTCHA_NOISE_FUNCTIONS = getattr(settings, 'CAPTCHA_NOISE_FUNCTIONS', ('captcha.helpers.noise_arcs', 'captcha.helpers.noise_dots',))
CAPTCHA_FILTER_FUNCTIONS = getattr(settings, 'CAPTCHA_FILTER_FUNCTIONS', ('captcha.helpers.post_smooth',))
CAPTCHA_WORDS_DICTIONARY = getattr(settings, 'CAPTCHA_WORDS_DICTIONARY', '/usr/share/dict/words')
CAPTCHA_PUNCTUATION = getattr(settings, 'CAPTCHA_PUNCTUATION', '''_"',.;:-''')
CAPTCHA_FLITE_PATH = getattr(settings, 'CAPTCHA_FLITE_PATH', None)
CAPTCHA_TIMEOUT = getattr(settings, 'CAPTCHA_TIMEOUT', 5) # Minutes
CAPTCHA_LENGTH = int(getattr(settings, 'CAPTCHA_LENGTH', 4)) # Chars
CAPTCHA_IMAGE_BEFORE_FIELD = getattr(settings, 'CAPTCHA_IMAGE_BEFORE_FIELD', True)
CAPTCHA_DICTIONARY_MIN_LENGTH = getattr(settings, 'CAPTCHA_DICTIONARY_MIN_LENGTH', 0)
CAPTCHA_DICTIONARY_MAX_LENGTH = getattr(settings, 'CAPTCHA_DICTIONARY_MAX_LENGTH', 99)
if CAPTCHA_IMAGE_BEFORE_FIELD:
CAPTCHA_OUTPUT_FORMAT = getattr(settings, 'CAPTCHA_OUTPUT_FORMAT', u'%(image)s %(hidden_field)s %(text_field)s')
else:
CAPTCHA_OUTPUT_FORMAT = getattr(settings, 'CAPTCHA_OUTPUT_FORMAT', u'%(hidden_field)s %(text_field)s %(image)s')
CAPTCHA_TEST_MODE = getattr(settings, 'CAPTCHA_TEST_MODE', getattr(settings, 'CATPCHA_TEST_MODE', False))
# Failsafe
if CAPTCHA_DICTIONARY_MIN_LENGTH > CAPTCHA_DICTIONARY_MAX_LENGTH:
CAPTCHA_DICTIONARY_MIN_LENGTH, CAPTCHA_DICTIONARY_MAX_LENGTH = CAPTCHA_DICTIONARY_MAX_LENGTH, CAPTCHA_DICTIONARY_MIN_LENGTH
def _callable_from_string(string_or_callable):
if callable(string_or_callable):
return string_or_callable
else:
return getattr(__import__('.'.join(string_or_callable.split('.')[:-1]), {}, {}, ['']), string_or_callable.split('.')[-1])
def get_challenge():
return _callable_from_string(CAPTCHA_CHALLENGE_FUNCT)
def noise_functions():
if CAPTCHA_NOISE_FUNCTIONS:
return map(_callable_from_string, CAPTCHA_NOISE_FUNCTIONS)
return []
def filter_functions():
if CAPTCHA_FILTER_FUNCTIONS:
return map(_callable_from_string, CAPTCHA_FILTER_FUNCTIONS)
return []

135
thirdpart/captcha/fields.py Normal file
View File

@@ -0,0 +1,135 @@
from captcha.conf import settings
from django.conf import settings as django_settings
from captcha.models import CaptchaStore, get_safe_now
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse, NoReverseMatch
from django.forms import ValidationError
from django.forms.fields import CharField, MultiValueField
from django.forms.widgets import TextInput, MultiWidget, HiddenInput
from django.utils.translation import ugettext, ugettext_lazy
class BaseCaptchaTextInput(MultiWidget):
"""
Base class for Captcha widgets
"""
def __init__(self, attrs=None):
widgets = (
HiddenInput(attrs),
TextInput(attrs),
)
super(BaseCaptchaTextInput, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
return value.split(',')
return [None, None]
def fetch_captcha_store(self, name, value, attrs=None):
"""
Fetches a new CaptchaStore
This has to be called inside render
"""
try:
reverse('captcha-image', args=('dummy',))
except NoReverseMatch:
raise ImproperlyConfigured('Make sure you\'ve included captcha.urls as explained in the INSTALLATION section on http://readthedocs.org/docs/django-simple-captcha/en/latest/usage.html#installation')
key = CaptchaStore.generate_key()
# these can be used by format_output and render
self._value = [key, u'']
self._key = key
self.id_ = self.build_attrs(attrs).get('id', None)
def render(self, name, value, attrs=None):
#self.fetch_captcha_store(name, value, attrs)
return super(BaseCaptchaTextInput, self).render(name, self._value, attrs=attrs)
def id_for_label(self, id_):
if id_:
return id_ + '_1'
return id_
def image_url(self):
return reverse('captcha-image', kwargs={'key': self._key})
def audio_url(self):
return reverse('captcha-audio', kwargs={'key': self._key}) if settings.CAPTCHA_FLITE_PATH else None
def refresh_url(self):
return reverse('captcha-refresh')
class CaptchaTextInput(BaseCaptchaTextInput):
def __init__(self, attrs=None, **kwargs):
self._args = kwargs
self._args['output_format'] = self._args.get('output_format') or settings.CAPTCHA_OUTPUT_FORMAT
for key in ('image', 'hidden_field', 'text_field'):
if '%%(%s)s' % key not in self._args['output_format']:
raise ImproperlyConfigured('All of %s must be present in your CAPTCHA_OUTPUT_FORMAT setting. Could not find %s' % (
', '.join(['%%(%s)s' % k for k in ('image', 'hidden_field', 'text_field')]),
'%%(%s)s' % key
))
super(CaptchaTextInput, self).__init__(attrs)
def format_output(self, rendered_widgets):
hidden_field, text_field = rendered_widgets
return self._args['output_format'] % {
'image': self.image_and_audio,
'hidden_field': hidden_field,
'text_field': text_field
}
def render(self, name, value, attrs=None):
self.fetch_captcha_store(name, value, attrs)
self.image_and_audio = '<img src="%s" alt="captcha" class="captcha" />' % self.image_url()
if settings.CAPTCHA_FLITE_PATH:
self.image_and_audio = '<a href="%s" title="%s">%s</a>' % (self.audio_url(), ugettext('Play CAPTCHA as audio file'), self.image_and_audio)
return super(CaptchaTextInput, self).render(name, self._value, attrs=attrs)
class CaptchaField(MultiValueField):
def __init__(self, *args, **kwargs):
fields = (
CharField(show_hidden_initial=True),
CharField(),
)
if 'error_messages' not in kwargs or 'invalid' not in kwargs.get('error_messages'):
if 'error_messages' not in kwargs:
kwargs['error_messages'] = {}
kwargs['error_messages'].update({'invalid': ugettext_lazy('Invalid CAPTCHA')})
kwargs['widget'] = kwargs.pop('widget', CaptchaTextInput(output_format=kwargs.pop('output_format', None)))
super(CaptchaField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
if data_list:
return ','.join(data_list)
return None
def clean(self, value):
super(CaptchaField, self).clean(value)
response, value[1] = (value[1] or '').strip().lower(), ''
CaptchaStore.remove_expired()
if settings.CAPTCHA_TEST_MODE and response.lower() == 'passed':
# automatically pass the test
try:
# try to delete the captcha based on its hash
CaptchaStore.objects.get(hashkey=value[0]).delete()
except CaptchaStore.DoesNotExist:
# ignore errors
pass
elif not self.required and not response:
pass
else:
try:
CaptchaStore.objects.get(response=response, hashkey=value[0], expiration__gt=get_safe_now()).delete()
except CaptchaStore.DoesNotExist:
raise ValidationError(getattr(self, 'error_messages', {}).get('invalid', ugettext_lazy('Invalid CAPTCHA')))
return value

View File

@@ -0,0 +1,124 @@
Bitstream Vera Fonts Copyright
The fonts have a generous copyright, allowing derivative works (as
long as "Bitstream" or "Vera" are not in the names), and full
redistribution (so long as they are not *sold* by themselves). They
can be be bundled, redistributed and sold with any software.
The fonts are distributed under the following copyright:
Copyright
=========
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream
Vera is a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of the fonts accompanying this license ("Fonts") and associated
documentation files (the "Font Software"), to reproduce and distribute
the Font Software, including without limitation the rights to use,
copy, merge, publish, distribute, and/or sell copies of the Font
Software, and to permit persons to whom the Font Software is furnished
to do so, subject to the following conditions:
The above copyright and trademark notices and this permission notice
shall be included in all copies of one or more of the Font Software
typefaces.
The Font Software may be modified, altered, or added to, and in
particular the designs of glyphs or characters in the Fonts may be
modified and additional glyphs or characters may be added to the
Fonts, only if the fonts are renamed to names not containing either
the words "Bitstream" or the word "Vera".
This License becomes null and void to the extent applicable to Fonts
or Font Software that has been modified and is distributed under the
"Bitstream Vera" names.
The Font Software may be sold as part of a larger software package but
no copy of one or more of the Font Software typefaces may be sold by
itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT
SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome
Foundation, and Bitstream Inc., shall not be used in advertising or
otherwise to promote the sale, use or other dealings in this Font
Software without prior written authorization from the Gnome Foundation
or Bitstream Inc., respectively. For further information, contact:
fonts at gnome dot org.
Copyright FAQ
=============
1. I don't understand the resale restriction... What gives?
Bitstream is giving away these fonts, but wishes to ensure its
competitors can't just drop the fonts as is into a font sale system
and sell them as is. It seems fair that if Bitstream can't make money
from the Bitstream Vera fonts, their competitors should not be able to
do so either. You can sell the fonts as part of any software package,
however.
2. I want to package these fonts separately for distribution and
sale as part of a larger software package or system. Can I do so?
Yes. A RPM or Debian package is a "larger software package" to begin
with, and you aren't selling them independently by themselves.
See 1. above.
3. Are derivative works allowed?
Yes!
4. Can I change or add to the font(s)?
Yes, but you must change the name(s) of the font(s).
5. Under what terms are derivative works allowed?
You must change the name(s) of the fonts. This is to ensure the
quality of the fonts, both to protect Bitstream and Gnome. We want to
ensure that if an application has opened a font specifically of these
names, it gets what it expects (though of course, using fontconfig,
substitutions could still could have occurred during font
opening). You must include the Bitstream copyright. Additional
copyrights can be added, as per copyright law. Happy Font Hacking!
6. If I have improvements for Bitstream Vera, is it possible they might get
adopted in future versions?
Yes. The contract between the Gnome Foundation and Bitstream has
provisions for working with Bitstream to ensure quality additions to
the Bitstream Vera font family. Please contact us if you have such
additions. Note, that in general, we will want such additions for the
entire family, not just a single font, and that you'll have to keep
both Gnome and Jim Lyles, Vera's designer, happy! To make sense to add
glyphs to the font, they must be stylistically in keeping with Vera's
design. Vera cannot become a "ransom note" font. Jim Lyles will be
providing a document describing the design elements used in Vera, as a
guide and aid for people interested in contributing to Vera.
7. I want to sell a software package that uses these fonts: Can I do so?
Sure. Bundle the fonts with your software and sell your software
with the fonts. That is the intent of the copyright.
8. If applications have built the names "Bitstream Vera" into them,
can I override this somehow to use fonts of my choosing?
This depends on exact details of the software. Most open source
systems and software (e.g., Gnome, KDE, etc.) are now converting to
use fontconfig (see www.fontconfig.org) to handle font configuration,
selection and substitution; it has provisions for overriding font
names and subsituting alternatives. An example is provided by the
supplied local.conf file, which chooses the family Bitstream Vera for
"sans", "serif" and "monospace". Other software (e.g., the XFree86
core server) has other mechanisms for font substitution.

View File

@@ -0,0 +1,11 @@
Contained herin is the Bitstream Vera font family.
The Copyright information is found in the COPYRIGHT.TXT file (along
with being incoporated into the fonts themselves).
The releases notes are found in the file "RELEASENOTES.TXT".
We hope you enjoy Vera!
Bitstream, Inc.
The Gnome Project

Binary file not shown.

View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
import random
from captcha.conf import settings
from django.core.urlresolvers import reverse
def math_challenge():
operators = ('+', '*', '-',)
operands = (random.randint(1, 10), random.randint(1, 10))
operator = random.choice(operators)
if operands[0] < operands[1] and '-' == operator:
operands = (operands[1], operands[0])
challenge = '%d%s%d' % (operands[0], operator, operands[1])
return '%s=' % (challenge), eval(challenge)
def random_char_challenge():
chars, ret = u'abcdefghijklmnopqrstuvwxyz', u''
for i in range(settings.CAPTCHA_LENGTH):
ret += random.choice(chars)
return ret.upper(), ret
def unicode_challenge():
chars, ret = u'äàáëéèïíîöóòüúù', u''
for i in range(settings.CAPTCHA_LENGTH):
ret += random.choice(chars)
return ret.upper(), ret
def word_challenge():
fd = open(settings.CAPTCHA_WORDS_DICTIONARY, 'rb')
l = fd.readlines()
fd.close()
while True:
word = random.choice(l).strip()
if len(word) >= settings.CAPTCHA_DICTIONARY_MIN_LENGTH and len(word) <= settings.CAPTCHA_DICTIONARY_MAX_LENGTH:
break
return word.upper(), word.lower()
def huge_words_and_punctuation_challenge():
"Yay, undocumneted. Mostly used to test Issue 39 - http://code.google.com/p/django-simple-captcha/issues/detail?id=39"
fd = open(settings.CAPTCHA_WORDS_DICTIONARY, 'rb')
l = fd.readlines()
fd.close()
word = ''
while True:
word1 = random.choice(l).strip()
word2 = random.choice(l).strip()
punct = random.choice(settings.CAPTCHA_PUNCTUATION)
word = '%s%s%s' % (word1, punct, word2)
if len(word) >= settings.CAPTCHA_DICTIONARY_MIN_LENGTH and len(word) <= settings.CAPTCHA_DICTIONARY_MAX_LENGTH:
break
return word.upper(), word.lower()
def noise_arcs(draw, image):
size = image.size
draw.arc([-20, -20, size[0], 20], 0, 295, fill=settings.CAPTCHA_FOREGROUND_COLOR)
draw.line([-20, 20, size[0] + 20, size[1] - 20], fill=settings.CAPTCHA_FOREGROUND_COLOR)
draw.line([-20, 0, size[0] + 20, size[1]], fill=settings.CAPTCHA_FOREGROUND_COLOR)
return draw
def noise_dots(draw, image):
size = image.size
for p in range(int(size[0] * size[1] * 0.1)):
draw.point((random.randint(0, size[0]), random.randint(0, size[1])), fill=settings.CAPTCHA_FOREGROUND_COLOR)
return draw
def post_smooth(image):
try:
import ImageFilter
except ImportError:
from PIL import ImageFilter
return image.filter(ImageFilter.SMOOTH)
def captcha_image_url(key):
""" Return url to image. Need for ajax refresh and, etc"""
return reverse('captcha-image', args=[key])

View File

@@ -0,0 +1,30 @@
# Czech translation of django-simple-captcha.
# Copyright (C) 2012 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Beda Kosata <bedrich.kosata@nic.cz>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.3.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-10-09 07:05+0200\n"
"PO-Revision-Date: 2012-10-09 07:08+0200\n"
"Last-Translator: Beda Kosata <bedrich.kosata@nic.cz>\n"
"Language-Team: Czech <>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
#: fields.py:50
msgid "Play CAPTCHA as audio file"
msgstr "Přehrát captchu jako audio soubor"
#: fields.py:67 fields.py:99 tests/__init__.py:62
msgid "Invalid CAPTCHA"
msgstr "Neplatná CAPTCHA"
#: tests/__init__.py:88
msgid "This field is required."
msgstr "Toto pole je povinné."

View File

@@ -0,0 +1,31 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-07-16 12:06+0200\n"
"PO-Revision-Date: 2013-07-16 12:10+0100\n"
"Last-Translator: Patrick Lauber <patrick.lauber@divio.ch>\n"
"Language-Team: DE <digi@treepy.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.5.7\n"
#: fields.py:90
msgid "Play CAPTCHA as audio file"
msgstr "Spiel das CAPTCHA als Audiodatei ab."
#: fields.py:105 fields.py:134 tests/tests.py:99 tests/tests.py:239
#: tests/tests.py:246
msgid "Invalid CAPTCHA"
msgstr "Üngültiges CAPTCHA"
#: tests/tests.py:125
msgid "This field is required."
msgstr "Dieses Feld ist benötigt."

View File

@@ -0,0 +1,31 @@
# django-simple-captcha French translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Patrick Samson <maxcom@laposte.net>, 2010.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.2.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-07-25 11:44+0300\n"
"PO-Revision-Date: 2010-09-16 12:16+0200\n"
"Last-Translator: Patrick Samson <maxcom@laposte.net>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n>1;\n"
#: fields.py:49
msgid "Play CAPTCHA as audio file"
msgstr "Jouer en tant que fichier audio"
#: fields.py:66 fields.py:89 tests/__init__.py:62
msgid "Invalid CAPTCHA"
msgstr "Réponse invalide"
#: tests/__init__.py:88
msgid "This field is required."
msgstr "Ce champ est obligatoire."

View File

@@ -0,0 +1,29 @@
# django-simple-captcha Italian translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Arjuna Del Toso <stupidate@gmail.net>, 2012
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-11-14 02:53+0000\n"
"PO-Revision-Date: 2012-11-14 02:53+0000\n"
"Last-Translator: Arjuna Del Toso <stupidate@gmail.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n>1;\n"
#: .\fields.py:56
msgid "Play CAPTCHA as audio file"
msgstr "Ascolta la parola di controllo"
#: .\fields.py:71 .\fields.py:96 .\tests\__init__.py:70
msgid "Invalid CAPTCHA"
msgstr "Parola di controllo sbagliata"
#: .\tests\__init__.py:97
msgid "This field is required."
msgstr "Questo campo è obbligatorio"

View File

@@ -0,0 +1,30 @@
# django-simple-captcha Dutch translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Leon de Rijke <leon@cmsworks.nl>, 2013.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-02-15 13:26+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Leon de Rijke <leon@cmsworks.nl>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
#: fields.py:50
msgid "Play CAPTCHA as audio file"
msgstr "Speel CAPTCHA als audiobestand af"
#: fields.py:67 fields.py:94 tests/__init__.py:64 tests/__init__.py:186
#: tests/__init__.py:193
msgid "Invalid CAPTCHA"
msgstr "CAPTCHA ongeldig, probeer het opnieuw"
#: tests/__init__.py:90
msgid "This field is required."
msgstr "Dit veld is verplicht."

View File

@@ -0,0 +1,33 @@
# Polish translation for django-simple-captcha.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the django-simple-captcha package.
# Sławomir Zborowski <slawomir.zborowski@hotmail.com>, 2013.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-08-18 18:49+0200\n"
"PO-Revision-Date: 2013-08-18 18:52+0200\n"
"Last-Translator: Sławomir Zborowski <slawomir.zborowski@hotmail.com>\n"
"Language-Team: Polisch\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2)\n"
#: fields.py:90
msgid "Play CAPTCHA as audio file"
msgstr "Odtwórz CAPTCHĘ jako plik dźwiękowy"
#: fields.py:105 fields.py:134 tests/tests.py:99 tests/tests.py:239
#: tests/tests.py:246
msgid "Invalid CAPTCHA"
msgstr "Niepoprawnie wpisana CAPTCHA"
#: tests/tests.py:125
msgid "This field is required."
msgstr "To pole jest wymagane"

View File

@@ -0,0 +1,31 @@
# django-simple-captcha Portuguese (Brazilian) translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Alisson Patricio <eu@alisson.net>, 2013.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.7\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-05-18 10:58-0300\n"
"PO-Revision-Date: 2013-05-18 13:12-0300\n"
"Last-Translator: Alisson Patricio <eu@alisson.net>\n"
"Language: Portuguese (Brazilian)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: fields.py:49
msgid "Play CAPTCHA as audio file"
msgstr "Ouça o arquivo de áudio"
#: fields.py:66 fields.py:93 tests/__init__.py:69
#: tests/__init__.py:198 tests/__init__.py:205
msgid "Invalid CAPTCHA"
msgstr "Resposta inválida"
#: tests/__init__.py:95
msgid "This field is required."
msgstr "Este campo é obrigatório."

View File

@@ -0,0 +1,32 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-07-25 11:17+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
#: fields.py:49
msgid "Play CAPTCHA as audio file"
msgstr "Воспроизвести CAPTCHA в виде аудио файла"
#: fields.py:66 fields.py:89 tests/__init__.py:62
msgid "Invalid CAPTCHA"
msgstr "Неверный ответ"
#: tests/__init__.py:88
msgid "This field is required."
msgstr "Это поле обязательно для заполнения."

View File

@@ -0,0 +1,31 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-01-19 16:33-0200\n"
"PO-Revision-Date: 2013-01-19 20:52+0200\n"
"Last-Translator: Gokmen Gorgen <gokmen@alageek.com>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0\n"
#: fields.py:50
msgid "Play CAPTCHA as audio file"
msgstr "Değeri ses dosyası olarak çal"
#: fields.py:67 fields.py:94 tests/__init__.py:64 tests/__init__.py:186
#: tests/__init__.py:193
msgid "Invalid CAPTCHA"
msgstr "Geçersiz değer"
#: tests/__init__.py:90
msgid "This field is required."
msgstr "Bu alan zorunludur."

View File

@@ -0,0 +1,32 @@
# django-simple-captcha Chinese Simplified translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Ming Chen <c.ming@live.com>, 2013.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-03-01 05:04+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Ming Chen <c.ming@live.com>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0\n"
#: fields.py:49
msgid "Play CAPTCHA as audio file"
msgstr "使用语音方式播放认证码"
#: fields.py:66 fields.py:93 tests/__init__.py:69 tests/__init__.py:198
#: tests/__init__.py:205
msgid "Invalid CAPTCHA"
msgstr "认证码错误"
#: tests/__init__.py:95
msgid "This field is required."
msgstr "这个字段是必须的"

View File

View File

@@ -0,0 +1,25 @@
from django.core.management.base import BaseCommand
from captcha.models import get_safe_now
import sys
class Command(BaseCommand):
help = "Clean up expired captcha hashkeys."
def handle(self, **options):
from captcha.models import CaptchaStore
verbose = int(options.get('verbosity'))
expired_keys = CaptchaStore.objects.filter(expiration__lte=get_safe_now()).count()
if verbose >= 1:
print "Currently %d expired hashkeys" % expired_keys
try:
CaptchaStore.remove_expired()
except:
if verbose >= 1:
print "Unable to delete expired hashkeys."
sys.exit(1)
if verbose >= 1:
if expired_keys > 0:
print "%d expired hashkeys removed." % expired_keys
else:
print "No keys to remove."

View File

@@ -0,0 +1,39 @@
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'CaptchaStore'
db.create_table('captcha_captchastore', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('challenge', self.gf('django.db.models.fields.CharField')(max_length=32)),
('response', self.gf('django.db.models.fields.CharField')(max_length=32)),
('hashkey', self.gf('django.db.models.fields.CharField')(unique=True, max_length=40)),
('expiration', self.gf('django.db.models.fields.DateTimeField')()),
))
db.send_create_signal('captcha', ['CaptchaStore'])
def backwards(self, orm):
# Deleting model 'CaptchaStore'
db.delete_table('captcha_captchastore')
models = {
'captcha.captchastore': {
'Meta': {'object_name': 'CaptchaStore'},
'challenge': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'expiration': ('django.db.models.fields.DateTimeField', [], {}),
'hashkey': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'response': ('django.db.models.fields.CharField', [], {'max_length': '32'})
}
}
complete_apps = ['captcha']

View File

View File

@@ -0,0 +1,69 @@
from captcha.conf import settings as captcha_settings
from django.db import models
from django.conf import settings
import datetime
import random
import time
import unicodedata
import six
# Heavily based on session key generation in Django
# Use the system (hardware-based) random number generator if it exists.
if hasattr(random, 'SystemRandom'):
randrange = random.SystemRandom().randrange
else:
randrange = random.randrange
MAX_RANDOM_KEY = 18446744073709551616 # 2 << 63
try:
import hashlib # sha for Python 2.5+
except ImportError:
import sha # sha for Python 2.4 (deprecated in Python 2.6)
hashlib = False
def get_safe_now():
try:
from django.utils.timezone import utc
if settings.USE_TZ:
return datetime.datetime.utcnow().replace(tzinfo=utc)
except:
pass
return datetime.datetime.now()
class CaptchaStore(models.Model):
challenge = models.CharField(blank=False, max_length=32)
response = models.CharField(blank=False, max_length=32)
hashkey = models.CharField(blank=False, max_length=40, unique=True)
expiration = models.DateTimeField(blank=False)
def save(self, *args, **kwargs):
#import ipdb; ipdb.set_trace()
self.response = six.text_type(self.response).lower()
if not self.expiration:
#self.expiration = datetime.datetime.now() + datetime.timedelta(minutes=int(captcha_settings.CAPTCHA_TIMEOUT))
self.expiration = get_safe_now() + datetime.timedelta(minutes=int(captcha_settings.CAPTCHA_TIMEOUT))
if not self.hashkey:
key_ = unicodedata.normalize('NFKD', str(randrange(0, MAX_RANDOM_KEY)) + str(time.time()) + six.text_type(self.challenge)).encode('ascii', 'ignore') + unicodedata.normalize('NFKD', six.text_type(self.response)).encode('ascii', 'ignore')
if hashlib:
self.hashkey = hashlib.sha1(key_).hexdigest()
else:
self.hashkey = sha.new(key_).hexdigest()
del(key_)
super(CaptchaStore, self).save(*args, **kwargs)
def __unicode__(self):
return self.challenge
def remove_expired(cls):
cls.objects.filter(expiration__lte=get_safe_now()).delete()
remove_expired = classmethod(remove_expired)
@classmethod
def generate_key(cls):
challenge, response = captcha_settings.get_challenge()()
store = cls.objects.create(challenge=challenge, response=response)
return store.hashkey

View File

@@ -0,0 +1 @@
from .tests import CaptchaCase, trivial_challenge

View File

@@ -0,0 +1,280 @@
# -*- coding: utf-8 -*-
from captcha.conf import settings
from captcha.fields import CaptchaField, CaptchaTextInput
from captcha.models import CaptchaStore, get_safe_now
from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.utils.translation import ugettext_lazy
import datetime
import json
import re
import six
import os
class CaptchaCase(TestCase):
urls = 'captcha.tests.urls'
def setUp(self):
self.stores = {}
self.__current_settings_output_format = settings.CAPTCHA_OUTPUT_FORMAT
self.__current_settings_dictionary = settings.CAPTCHA_WORDS_DICTIONARY
self.__current_settings_punctuation = settings.CAPTCHA_PUNCTUATION
tested_helpers = ['captcha.helpers.math_challenge', 'captcha.helpers.random_char_challenge', 'captcha.helpers.unicode_challenge']
if os.path.exists('/usr/share/dict/words'):
settings.CAPTCHA_WORDS_DICTIONARY = '/usr/share/dict/words'
settings.CAPTCHA_PUNCTUATION = ';-,.'
tested_helpers.append('captcha.helpers.word_challenge')
tested_helpers.append('captcha.helpers.huge_words_and_punctuation_challenge')
for helper in tested_helpers:
challenge, response = settings._callable_from_string(helper)()
self.stores[helper.rsplit('.', 1)[-1].replace('_challenge', '_store')], _ = CaptchaStore.objects.get_or_create(challenge=challenge, response=response)
challenge, response = settings.get_challenge()()
self.stores['default_store'], _ = CaptchaStore.objects.get_or_create(challenge=challenge, response=response)
self.default_store = self.stores['default_store']
def tearDown(self):
settings.CAPTCHA_OUTPUT_FORMAT = self.__current_settings_output_format
settings.CAPTCHA_WORDS_DICTIONARY = self.__current_settings_dictionary
settings.CAPTCHA_PUNCTUATION = self.__current_settings_punctuation
def __extract_hash_and_response(self, r):
hash_ = re.findall(r'value="([0-9a-f]+)"', str(r.content))[0]
response = CaptchaStore.objects.get(hashkey=hash_).response
return hash_, response
def testImages(self):
for key in [store.hashkey for store in six.itervalues(self.stores)]:
response = self.client.get(reverse('captcha-image', kwargs=dict(key=key)))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.has_header('content-type'))
self.assertEqual(response._headers.get('content-type'), ('Content-Type', 'image/png'))
def testAudio(self):
if not settings.CAPTCHA_FLITE_PATH:
return
for key in (self.stores.get('math_store').hashkey, self.stores.get('math_store').hashkey, self.default_store.hashkey):
response = self.client.get(reverse('captcha-audio', kwargs=dict(key=key)))
self.assertEqual(response.status_code, 200)
self.assertTrue(len(response.content) > 1024)
self.assertTrue(response.has_header('content-type'))
self.assertEqual(response._headers.get('content-type'), ('Content-Type', 'audio/x-wav'))
def testFormSubmit(self):
r = self.client.get(reverse('captcha-test'))
self.assertEqual(r.status_code, 200)
hash_, response = self.__extract_hash_and_response(r)
r = self.client.post(reverse('captcha-test'), dict(captcha_0=hash_, captcha_1=response, subject='xxx', sender='asasd@asdasd.com'))
self.assertEqual(r.status_code, 200)
self.assertTrue(str(r.content).find('Form validated') > 0)
r = self.client.post(reverse('captcha-test'), dict(captcha_0=hash_, captcha_1=response, subject='xxx', sender='asasd@asdasd.com'))
self.assertEqual(r.status_code, 200)
self.assertFalse(str(r.content).find('Form validated') > 0)
def testFormModelForm(self):
r = self.client.get(reverse('captcha-test-model-form'))
self.assertEqual(r.status_code, 200)
hash_, response = self.__extract_hash_and_response(r)
r = self.client.post(reverse('captcha-test-model-form'), dict(captcha_0=hash_, captcha_1=response, subject='xxx', sender='asasd@asdasd.com'))
self.assertEqual(r.status_code, 200)
self.assertTrue(str(r.content).find('Form validated') > 0)
r = self.client.post(reverse('captcha-test-model-form'), dict(captcha_0=hash_, captcha_1=response, subject='xxx', sender='asasd@asdasd.com'))
self.assertEqual(r.status_code, 200)
self.assertFalse(str(r.content).find('Form validated') > 0)
def testWrongSubmit(self):
for urlname in ('captcha-test', 'captcha-test-model-form'):
r = self.client.get(reverse(urlname))
self.assertEqual(r.status_code, 200)
r = self.client.post(reverse(urlname), dict(captcha_0='abc', captcha_1='wrong response', subject='xxx', sender='asasd@asdasd.com'))
self.assertFormError(r, 'form', 'captcha', ugettext_lazy('Invalid CAPTCHA'))
def testDeleteExpired(self):
self.default_store.expiration = get_safe_now() - datetime.timedelta(minutes=5)
self.default_store.save()
hash_ = self.default_store.hashkey
r = self.client.post(reverse('captcha-test'), dict(captcha_0=hash_, captcha_1=self.default_store.response, subject='xxx', sender='asasd@asdasd.com'))
self.assertEqual(r.status_code, 200)
self.assertFalse('Form validated' in str(r.content))
# expired -> deleted
try:
CaptchaStore.objects.get(hashkey=hash_)
self.fail()
except:
pass
def testCustomErrorMessage(self):
r = self.client.get(reverse('captcha-test-custom-error-message'))
self.assertEqual(r.status_code, 200)
# Wrong answer
r = self.client.post(reverse('captcha-test-custom-error-message'), dict(captcha_0='abc', captcha_1='wrong response'))
self.assertFormError(r, 'form', 'captcha', 'TEST CUSTOM ERROR MESSAGE')
# empty answer
r = self.client.post(reverse('captcha-test-custom-error-message'), dict(captcha_0='abc', captcha_1=''))
self.assertFormError(r, 'form', 'captcha', ugettext_lazy('This field is required.'))
def testRepeatedChallenge(self):
CaptchaStore.objects.create(challenge='xxx', response='xxx')
try:
CaptchaStore.objects.create(challenge='xxx', response='xxx')
except Exception:
self.fail()
def testRepeatedChallengeFormSubmit(self):
__current_challange_function = settings.CAPTCHA_CHALLENGE_FUNCT
for urlname in ('captcha-test', 'captcha-test-model-form'):
settings.CAPTCHA_CHALLENGE_FUNCT = 'captcha.tests.trivial_challenge'
r1 = self.client.get(reverse(urlname))
r2 = self.client.get(reverse(urlname))
self.assertEqual(r1.status_code, 200)
self.assertEqual(r2.status_code, 200)
if re.findall(r'value="([0-9a-f]+)"', str(r1.content)):
hash_1 = re.findall(r'value="([0-9a-f]+)"', str(r1.content))[0]
else:
self.fail()
if re.findall(r'value="([0-9a-f]+)"', str(r2.content)):
hash_2 = re.findall(r'value="([0-9a-f]+)"', str(r2.content))[0]
else:
self.fail()
try:
store_1 = CaptchaStore.objects.get(hashkey=hash_1)
store_2 = CaptchaStore.objects.get(hashkey=hash_2)
except:
self.fail()
self.assertTrue(store_1.pk != store_2.pk)
self.assertTrue(store_1.response == store_2.response)
self.assertTrue(hash_1 != hash_2)
r1 = self.client.post(reverse(urlname), dict(captcha_0=hash_1, captcha_1=store_1.response, subject='xxx', sender='asasd@asdasd.com'))
self.assertEqual(r1.status_code, 200)
self.assertTrue(str(r1.content).find('Form validated') > 0)
try:
store_2 = CaptchaStore.objects.get(hashkey=hash_2)
except:
self.fail()
r2 = self.client.post(reverse(urlname), dict(captcha_0=hash_2, captcha_1=store_2.response, subject='xxx', sender='asasd@asdasd.com'))
self.assertEqual(r2.status_code, 200)
self.assertTrue(str(r2.content).find('Form validated') > 0)
settings.CAPTCHA_CHALLENGE_FUNCT = __current_challange_function
def testOutputFormat(self):
for urlname in ('captcha-test', 'captcha-test-model-form'):
settings.CAPTCHA_OUTPUT_FORMAT = u'%(image)s<p>Hello, captcha world</p>%(hidden_field)s%(text_field)s'
r = self.client.get(reverse(urlname))
self.assertEqual(r.status_code, 200)
self.assertTrue('<p>Hello, captcha world</p>' in str(r.content))
def testInvalidOutputFormat(self):
__current_settings_debug = django_settings.DEBUG
for urlname in ('captcha-test', 'captcha-test-model-form'):
# we turn on DEBUG because CAPTCHA_OUTPUT_FORMAT is only checked debug
django_settings.DEBUG = True
settings.CAPTCHA_OUTPUT_FORMAT = u'%(image)s'
try:
self.client.get(reverse(urlname))
self.fail()
except ImproperlyConfigured as e:
self.assertTrue('CAPTCHA_OUTPUT_FORMAT' in str(e))
django_settings.DEBUG = __current_settings_debug
def testPerFormFormat(self):
settings.CAPTCHA_OUTPUT_FORMAT = u'%(image)s testCustomFormatString %(hidden_field)s %(text_field)s'
r = self.client.get(reverse('captcha-test'))
self.assertTrue('testCustomFormatString' in str(r.content))
r = self.client.get(reverse('test_per_form_format'))
self.assertTrue('testPerFieldCustomFormatString' in str(r.content))
def testIssue31ProperLabel(self):
settings.CAPTCHA_OUTPUT_FORMAT = u'%(image)s %(hidden_field)s %(text_field)s'
r = self.client.get(reverse('captcha-test'))
self.assertTrue('<label for="id_captcha_1"' in str(r.content))
def testRefreshView(self):
r = self.client.get(reverse('captcha-refresh'), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
try:
new_data = json.loads(six.text_type(r.content, encoding='ascii'))
self.assertTrue('image_url' in new_data)
except:
self.fail()
def testContentLength(self):
for key in [store.hashkey for store in six.itervalues(self.stores)]:
response = self.client.get(reverse('captcha-image', kwargs=dict(key=key)))
self.assertTrue(response.has_header('content-length'))
self.assertTrue(response['content-length'].isdigit())
self.assertTrue(int(response['content-length']))
def testIssue12ProperInstantiation(self):
"""
This test covers a default django field and widget behavior
It not assert anything. If something is wrong it will raise a error!
"""
settings.CAPTCHA_OUTPUT_FORMAT = u'%(image)s %(hidden_field)s %(text_field)s'
widget = CaptchaTextInput(attrs={'class': 'required'})
CaptchaField(widget=widget)
def testTestMode_Issue15(self):
__current_test_mode_setting = settings.CAPTCHA_TEST_MODE
settings.CAPTCHA_TEST_MODE = False
r = self.client.get(reverse('captcha-test'))
self.assertEqual(r.status_code, 200)
r = self.client.post(reverse('captcha-test'), dict(captcha_0='abc', captcha_1='wrong response', subject='xxx', sender='asasd@asdasd.com'))
self.assertFormError(r, 'form', 'captcha', ugettext_lazy('Invalid CAPTCHA'))
settings.CAPTCHA_TEST_MODE = True
# Test mode, only 'PASSED' is accepted
r = self.client.get(reverse('captcha-test'))
self.assertEqual(r.status_code, 200)
r = self.client.post(reverse('captcha-test'), dict(captcha_0='abc', captcha_1='wrong response', subject='xxx', sender='asasd@asdasd.com'))
self.assertFormError(r, 'form', 'captcha', ugettext_lazy('Invalid CAPTCHA'))
r = self.client.get(reverse('captcha-test'))
self.assertEqual(r.status_code, 200)
r = self.client.post(reverse('captcha-test'), dict(captcha_0='abc', captcha_1='passed', subject='xxx', sender='asasd@asdasd.com'))
self.assertTrue(str(r.content).find('Form validated') > 0)
settings.CAPTCHA_TEST_MODE = __current_test_mode_setting
def test_get_version(self):
import captcha
captcha.get_version(True)
def test_missing_value(self):
r = self.client.get(reverse('captcha-test-non-required'))
self.assertEqual(r.status_code, 200)
hash_, response = self.__extract_hash_and_response(r)
# Empty response is okay when required is False
r = self.client.post(reverse('captcha-test-non-required'), dict(subject='xxx', sender='asasd@asdasd.com'))
self.assertEqual(r.status_code, 200)
self.assertTrue(str(r.content).find('Form validated') > 0)
# But a valid response is okay, too
r = self.client.get(reverse('captcha-test-non-required'))
self.assertEqual(r.status_code, 200)
hash_, response = self.__extract_hash_and_response(r)
r = self.client.post(reverse('captcha-test-non-required'), dict(captcha_0=hash_, captcha_1=response, subject='xxx', sender='asasd@asdasd.com'))
self.assertEqual(r.status_code, 200)
self.assertTrue(str(r.content).find('Form validated') > 0)
def trivial_challenge():
return 'trivial', 'trivial'

View File

@@ -0,0 +1,13 @@
try:
from django.conf.urls import url, patterns, include
except ImportError:
from django.conf.urls.defaults import url, patterns, include
urlpatterns = patterns('',
url(r'test/$', 'captcha.tests.views.test', name='captcha-test'),
url(r'test-modelform/$', 'captcha.tests.views.test_model_form', name='captcha-test-model-form'),
url(r'test2/$', 'captcha.tests.views.test_custom_error_message', name='captcha-test-custom-error-message'),
url(r'test3/$', 'captcha.tests.views.test_per_form_format', name='test_per_form_format'),
url(r'test-non-required/$', 'captcha.tests.views.test_non_required', name='captcha-test-non-required'),
url(r'', include('captcha.urls')),
)

View File

@@ -0,0 +1,95 @@
from django import forms
from captcha.fields import CaptchaField
from django.template import RequestContext, loader
from django.http import HttpResponse
from django.contrib.auth.models import User
TEST_TEMPLATE = r'''
{% load url from future %}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>captcha test</title>
</head>
<body>
{% if passed %}
<p style="color:green">Form validated</p>
{% endif %}
{% if form.errors %}
{{form.errors}}
{% endif %}
<form action="{% url 'captcha-test' %}" method="post">
{{form.as_p}}
<p><input type="submit" value="Continue &rarr;"></p>
</form>
</body>
</html>
'''
def _test(request, form_class):
passed = False
if request.POST:
form = form_class(request.POST)
if form.is_valid():
passed = True
else:
form = form_class()
t = loader.get_template_from_string(TEST_TEMPLATE)
return HttpResponse(
t.render(RequestContext(request, dict(passed=passed, form=form))))
def test(request):
class CaptchaTestForm(forms.Form):
subject = forms.CharField(max_length=100)
sender = forms.EmailField()
captcha = CaptchaField(help_text='asdasd')
return _test(request, CaptchaTestForm)
def test_model_form(request):
class CaptchaTestModelForm(forms.ModelForm):
subject = forms.CharField(max_length=100)
sender = forms.EmailField()
captcha = CaptchaField(help_text='asdasd')
class Meta:
model = User
fields = ('subject', 'sender', 'captcha', )
return _test(request, CaptchaTestModelForm)
def test_custom_error_message(request):
class CaptchaTestErrorMessageForm(forms.Form):
captcha = CaptchaField(
help_text='asdasd',
error_messages=dict(invalid='TEST CUSTOM ERROR MESSAGE')
)
return _test(request, CaptchaTestErrorMessageForm)
def test_per_form_format(request):
class CaptchaTestFormatForm(forms.Form):
captcha = CaptchaField(
help_text='asdasd',
error_messages=dict(invalid='TEST CUSTOM ERROR MESSAGE'),
output_format=(
u'%(image)s testPerFieldCustomFormatString '
u'%(hidden_field)s %(text_field)s'
)
)
return _test(request, CaptchaTestFormatForm)
def test_non_required(request):
class CaptchaTestForm(forms.Form):
sender = forms.EmailField()
subject = forms.CharField(max_length=100)
captcha = CaptchaField(help_text='asdasd', required=False)
return _test(request, CaptchaTestForm)

11
thirdpart/captcha/urls.py Normal file
View File

@@ -0,0 +1,11 @@
try:
from django.conf.urls import patterns, url
except ImportError:
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('captcha.views',
url(r'image/(?P<key>\w+)/$', 'captcha_image', name='captcha-image', kwargs={'scale': 1}),
url(r'image/(?P<key>\w+)@2/$', 'captcha_image', name='captcha-image-2x', kwargs={'scale': 2}),
url(r'audio/(?P<key>\w+)/$', 'captcha_audio', name='captcha-audio'),
url(r'refresh/$', 'captcha_refresh', name='captcha-refresh'),
)

125
thirdpart/captcha/views.py Normal file
View File

@@ -0,0 +1,125 @@
from captcha.conf import settings
from captcha.helpers import captcha_image_url
from captcha.models import CaptchaStore
from django.http import HttpResponse, Http404
from django.shortcuts import get_object_or_404
import random
import re
import tempfile
import os
import subprocess
try:
from cStringIO import StringIO
except ImportError:
from io import BytesIO as StringIO
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
import Image
import ImageDraw
import ImageFont
try:
import json
except ImportError:
from django.utils import simplejson as json
NON_DIGITS_RX = re.compile('[^\d]')
def captcha_image(request, key, scale=1):
store = get_object_or_404(CaptchaStore, hashkey=key)
text = store.challenge
if settings.CAPTCHA_FONT_PATH.lower().strip().endswith('ttf'):
font = ImageFont.truetype(settings.CAPTCHA_FONT_PATH, settings.CAPTCHA_FONT_SIZE * scale)
else:
font = ImageFont.load(settings.CAPTCHA_FONT_PATH)
size = font.getsize(text)
size = (size[0] * 2, int(size[1] * 1.2))
image = Image.new('RGB', size, settings.CAPTCHA_BACKGROUND_COLOR)
try:
PIL_VERSION = int(NON_DIGITS_RX.sub('', Image.VERSION))
except:
PIL_VERSION = 116
xpos = 2
charlist = []
for char in text:
if char in settings.CAPTCHA_PUNCTUATION and len(charlist) >= 1:
charlist[-1] += char
else:
charlist.append(char)
for char in charlist:
fgimage = Image.new('RGB', size, settings.CAPTCHA_FOREGROUND_COLOR)
charimage = Image.new('L', font.getsize(' %s ' % char), '#000000')
chardraw = ImageDraw.Draw(charimage)
chardraw.text((0, 0), ' %s ' % char, font=font, fill='#ffffff')
if settings.CAPTCHA_LETTER_ROTATION:
if PIL_VERSION >= 116:
charimage = charimage.rotate(random.randrange(*settings.CAPTCHA_LETTER_ROTATION), expand=0, resample=Image.BICUBIC)
else:
charimage = charimage.rotate(random.randrange(*settings.CAPTCHA_LETTER_ROTATION), resample=Image.BICUBIC)
charimage = charimage.crop(charimage.getbbox())
maskimage = Image.new('L', size)
maskimage.paste(charimage, (xpos, 4, xpos + charimage.size[0], 4 + charimage.size[1]))
size = maskimage.size
image = Image.composite(fgimage, image, maskimage)
xpos = xpos + 2 + charimage.size[0]
image = image.crop((0, 0, xpos + 1, size[1]))
draw = ImageDraw.Draw(image)
for f in settings.noise_functions():
draw = f(draw, image)
for f in settings.filter_functions():
image = f(image)
out = StringIO()
image.save(out, "PNG")
out.seek(0)
response = HttpResponse(content_type='image/png')
response.write(out.read())
response['Content-length'] = out.tell()
return response
def captcha_audio(request, key):
if settings.CAPTCHA_FLITE_PATH:
store = get_object_or_404(CaptchaStore, hashkey=key)
text = store.challenge
if 'captcha.helpers.math_challenge' == settings.CAPTCHA_CHALLENGE_FUNCT:
text = text.replace('*', 'times').replace('-', 'minus')
else:
text = ', '.join(list(text))
path = str(os.path.join(tempfile.gettempdir(), '%s.wav' % key))
subprocess.call([settings.CAPTCHA_FLITE_PATH, "-t", text, "-o", path])
if os.path.isfile(path):
response = HttpResponse()
f = open(path, 'rb')
response['Content-Type'] = 'audio/x-wav'
response.write(f.read())
f.close()
os.unlink(path)
return response
raise Http404
def captcha_refresh(request):
""" Return json with new captcha for ajax refresh request """
if not request.is_ajax():
raise Http404
new_key = CaptchaStore.generate_key()
to_json_response = {
'key': new_key,
'image_url': captcha_image_url(new_key),
}
return HttpResponse(json.dumps(to_json_response), content_type='application/json')