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:
28
thirdpart/captcha/__init__.py
Normal file
28
thirdpart/captcha/__init__.py
Normal 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
|
0
thirdpart/captcha/conf/__init__.py
Normal file
0
thirdpart/captcha/conf/__init__.py
Normal file
53
thirdpart/captcha/conf/settings.py
Normal file
53
thirdpart/captcha/conf/settings.py
Normal 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
135
thirdpart/captcha/fields.py
Normal 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
|
124
thirdpart/captcha/fonts/COPYRIGHT.TXT
Normal file
124
thirdpart/captcha/fonts/COPYRIGHT.TXT
Normal 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.
|
||||||
|
|
11
thirdpart/captcha/fonts/README.TXT
Normal file
11
thirdpart/captcha/fonts/README.TXT
Normal 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
|
BIN
thirdpart/captcha/fonts/Vera.ttf
Normal file
BIN
thirdpart/captcha/fonts/Vera.ttf
Normal file
Binary file not shown.
83
thirdpart/captcha/helpers.py
Normal file
83
thirdpart/captcha/helpers.py
Normal 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])
|
30
thirdpart/captcha/locale/cs/LC_MESSAGES/django.po
Normal file
30
thirdpart/captcha/locale/cs/LC_MESSAGES/django.po
Normal 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é."
|
31
thirdpart/captcha/locale/de/LC_MESSAGES/django.po
Normal file
31
thirdpart/captcha/locale/de/LC_MESSAGES/django.po
Normal 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."
|
31
thirdpart/captcha/locale/fr/LC_MESSAGES/django.po
Normal file
31
thirdpart/captcha/locale/fr/LC_MESSAGES/django.po
Normal 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."
|
29
thirdpart/captcha/locale/it/LC_MESSAGES/django.po
Normal file
29
thirdpart/captcha/locale/it/LC_MESSAGES/django.po
Normal 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"
|
30
thirdpart/captcha/locale/nl/LC_MESSAGES/django.po
Normal file
30
thirdpart/captcha/locale/nl/LC_MESSAGES/django.po
Normal 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."
|
33
thirdpart/captcha/locale/pl/LC_MESSAGES/django.po
Normal file
33
thirdpart/captcha/locale/pl/LC_MESSAGES/django.po
Normal 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"
|
31
thirdpart/captcha/locale/pt_BR/LC_MESSAGES/django.po
Normal file
31
thirdpart/captcha/locale/pt_BR/LC_MESSAGES/django.po
Normal 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."
|
32
thirdpart/captcha/locale/ru/LC_MESSAGES/django.po
Normal file
32
thirdpart/captcha/locale/ru/LC_MESSAGES/django.po
Normal 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 "Это поле обязательно для заполнения."
|
31
thirdpart/captcha/locale/tr/LC_MESSAGES/django.po
Normal file
31
thirdpart/captcha/locale/tr/LC_MESSAGES/django.po
Normal 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."
|
32
thirdpart/captcha/locale/zh_CN/LC_MESSAGES/django.po
Normal file
32
thirdpart/captcha/locale/zh_CN/LC_MESSAGES/django.po
Normal 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 "这个字段是必须的"
|
||||||
|
|
0
thirdpart/captcha/management/__init__.py
Normal file
0
thirdpart/captcha/management/__init__.py
Normal file
0
thirdpart/captcha/management/commands/__init__.py
Normal file
0
thirdpart/captcha/management/commands/__init__.py
Normal file
25
thirdpart/captcha/management/commands/captcha_clean.py
Normal file
25
thirdpart/captcha/management/commands/captcha_clean.py
Normal 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."
|
39
thirdpart/captcha/migrations/0001_initial.py
Normal file
39
thirdpart/captcha/migrations/0001_initial.py
Normal 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']
|
0
thirdpart/captcha/migrations/__init__.py
Normal file
0
thirdpart/captcha/migrations/__init__.py
Normal file
69
thirdpart/captcha/models.py
Normal file
69
thirdpart/captcha/models.py
Normal 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
|
1
thirdpart/captcha/tests/__init__.py
Normal file
1
thirdpart/captcha/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .tests import CaptchaCase, trivial_challenge
|
280
thirdpart/captcha/tests/tests.py
Normal file
280
thirdpart/captcha/tests/tests.py
Normal 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'
|
13
thirdpart/captcha/tests/urls.py
Normal file
13
thirdpart/captcha/tests/urls.py
Normal 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')),
|
||||||
|
)
|
95
thirdpart/captcha/tests/views.py
Normal file
95
thirdpart/captcha/tests/views.py
Normal 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 →"></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
11
thirdpart/captcha/urls.py
Normal 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
125
thirdpart/captcha/views.py
Normal 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')
|
Reference in New Issue
Block a user