Commit a6e95021 authored by Aurel's avatar Aurel

add Captcha field, work done by Pierre


git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@32007 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 7c2a23dd
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2010 Nexedi SARL and Contributors. All Rights Reserved.
# Pierre Ducroquet <pierre.ducroquet@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
from Products.Formulator import Widget, Validator
from Products.Formulator.Field import ZMIField
from Products.Formulator.DummyField import fields
from Products.Formulator.Errors import ValidationError
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from AccessControl import ClassSecurityInfo
from Products.ERP5Type.Globals import DTMLFile
import CaptchasDotNet
import string
import random
import md5
import time
from zope.interface import Interface
from zope.interface import implements
_field_value_cache = {}
def purgeFieldValueCache():
_field_value_cache.clear()
class ICaptchaProvider(Interface):
"""The CaptchaProvider interface provides a captcha generator."""
def generate(self, field):
"""Returns a tuple (key, valid_answer) for this captcha.
That key is never sent directly to the client, it is always hashed before."""
def getHTML(self, field, captcha_key):
"""Returns the HTML code for the given captcha key"""
def getExtraPropertyList(self):
"""Returns the list of additionnary properties that are configurable"""
class CaptchasDotNetProvider(object):
implements(ICaptchaProvider)
def getImageGenerator (self, field):
captchas_client = field.get_value("captcha_dot_net_client") or "demo"
captchas_secret = field.get_value("captcha_dot_net_secret") or "secret"
return CaptchasDotNet.CaptchasDotNet(client = captchas_client, secret = captchas_secret)
def generate(self, field):
image_generator = self.getImageGenerator(field)
captcha_key = image_generator.random_string()
return (captcha_key, image_generator.get_answer(captcha_key))
def getHTML(self, field, captcha_key):
image_generator = self.getImageGenerator(field)
return image_generator.image(captcha_key, "__captcha_" + md5.new(captcha_key).hexdigest())
def getExtraPropertyList(self):
return [fields.StringField('captcha_dot_net_client',
title='Captchas.net client login',
description='Your login on captchas.net to get the pictures.',
default="demo",
size=32,
required=0),
fields.PasswordField('captcha_dot_net_secret',
title='Captchas.net client secret',
description='Your secret on captchas.net to get the pictures.',
default="secret",
size=32,
required=0)]
class NumericCaptchaProvider(object):
implements(ICaptchaProvider)
# No division because it would create decimal numbers
operator_set = {"+": "plus", "-": "minus", "*": "times"}
def generate(self, field):
# First step : generate the calculus. It is really simple.
terms = [str(random.randint(1, 20)), random.choice(self.operator_set.keys())]
#XXX: Find a way to prevent too complex captchas (for instance 11*7*19...)
#terms += [str(random.randint(1, 20)), random.choice(operator_set.keys())]
terms.append(str(random.randint(1, 20)))
# Second step : generate a text for it, and compute it
calculus_text = " ".join(terms)
result = eval(calculus_text)
return (calculus_text, result)
def getHTML(self, field, captcha_key):
# Make the text harder to parse for a computer
calculus_text = captcha_key
for (operator, replacement) in self.operator_set.items():
calculus_text = calculus_text.replace(operator, replacement)
return "<span class=\"%s\">%s</span>" % (field.get_value('css_class'), calculus_text)
def getExtraPropertyList(self):
return []
class CaptchaProviderFactory(object):
@staticmethod
def getProvider(name):
if name == "numeric":
return NumericCaptchaProvider()
elif name == "text":
return CaptchasDotNetProvider()
return None
@staticmethod
def getProviderList():
return [('Mathematics', 'numeric'), ('Text recognition (using captchas.net)', 'text')]
@staticmethod
def getDefaultProvider():
return "numeric"
class CaptchaWidget(Widget.TextWidget):
"""
A widget that displays a Captcha.
"""
def __init__(self):
# Associate a captcha key and (the right answer, the generation date)
self.__captcha_cache = {}
def add_captcha(self, key, value):
# First, cleanup the cache
cleanup_time = time.time() - 3600
for item in self.__captcha_cache.items():
if item[1][1] < cleanup_time:
del(self.__captcha_cache[item[0]])
# Then add the value if needed
if self.__captcha_cache.has_key(key):
return False
self.__captcha_cache[key] = (str(value), time.time())
return True
def validate_answer(self, key, value):
if not(self.__captcha_cache.has_key(key)):
return False
result = (self.__captcha_cache[key][0] == value)
del(self.__captcha_cache[key]) # Forbid several use of the same captcha.
return result
property_names = Widget.Widget.property_names + ['captcha_type']
captcha_type = fields.ListField('captcha_type',
title='Captcha type',
description=(
"The type of captcha you want to use."
""),
default=CaptchaProviderFactory.getDefaultProvider(),
required=1,
size=1,
items=CaptchaProviderFactory.getProviderList())
def render(self, field, key, value, REQUEST, render_prefix=None):
"""
Render editor
"""
captcha_key = None
captcha_field = None
captcha_type = field.get_value("captcha_type")
provider = CaptchaProviderFactory.getProvider(captcha_type)
(captcha_key, captcha_answer) = provider.generate(field)
while not(self.add_captcha(md5.new(captcha_key).hexdigest(), captcha_answer)):
(captcha_key, captcha_answer) = provider.generate(field)
captcha_field = provider.getHTML(field, captcha_key)
key_field = Widget.render_element("input",
type="hidden",
name="__captcha_" + key + "__",
value=md5.new(captcha_key).hexdigest()
)
splitter = "<br />"
answer = Widget.render_element("input",
type="text",
name=key,
css_class=field.get_value('css_class'),
size=10)
return captcha_field + key_field + splitter + answer
def render_view(self, field, value, REQUEST=None, render_prefix=None):
"""
Render form in view only mode.
"""
return None
CaptchaWidgetInstance = CaptchaWidget()
class CaptchaValidator(Validator.Validator):
message_names = Validator.Validator.message_names + ['wrong_captcha']
wrong_captcha = 'You did not enter the right answer.'
def validate(self, field, key, REQUEST):
value = REQUEST.get(key, None)
cache_key = REQUEST.get("__captcha_" + key + "__")
if not(CaptchaWidgetInstance.validate_answer(cache_key, value)):
self.raise_error('wrong_captcha', field)
return value
CaptchaValidatorInstance = CaptchaValidator()
class CaptchaField(ZMIField):
security = ClassSecurityInfo()
meta_type = "CaptchaField"
widget = CaptchaWidgetInstance
validator = CaptchaValidatorInstance
# methods screen
security.declareProtected('View management screens',
'manage_main')
manage_main = DTMLFile('dtml/captchaFieldEdit', globals())
security.declareProtected('Change Formulator Forms', 'manage_edit')
def manage_edit(self, REQUEST):
"""
Surcharged values for the captcha provider custom fields.
"""
captcha_provider = CaptchaProviderFactory.getProvider(self.get_value("captcha_type"))
result = {}
for field in captcha_provider.getExtraPropertyList():
try:
# validate the form and get results
result[field.get_real_field().id] = field.get_real_field().validate(REQUEST)
except ValidationError, err:
if REQUEST:
message = "Error: %s - %s" % (err.field.get_value('title'),
err.error_text)
return self.manage_main(self, REQUEST,
manage_tabs_message=message)
else:
raise
# Edit standards attributes
# XXX It is not possible to call ZMIField.manage_edit because
# it returns at the end...
# we need to had a parameter to the method
try:
# validate the form and get results
result.update(self.form.validate(REQUEST))
except ValidationError, err:
if REQUEST:
message = "Error: %s - %s" % (err.field.get_value('title'),
err.error_text)
return self.manage_main(self,REQUEST,
manage_tabs_message=message)
else:
raise
self.values.update(result)
self._edit(result)
# finally notify field of all changed values if necessary
for key in result:
method_name = "on_value_%s_changed" % key
if hasattr(self, method_name):
getattr(self, method_name)(result[key])
if REQUEST:
message="Content changed."
return self.manage_main(self, REQUEST,
manage_tabs_message=message)
def _edit(self, result):
if result.has_key("captcha_type"):
# Now, find out the old fields and wipe them out !
new_provider = CaptchaProviderFactory.getProvider(result["captcha_type"])
old_propertiesIds = self.__extraPropertyList
new_properties = [x.get_real_field() for x in new_provider.getExtraPropertyList()]
deleted_properties = [x for x in new_properties if not x.id in old_propertiesIds]
for deleted_property in deleted_properties:
if deleted_property.values.has_key("default"):
result[deleted_property.id] = deleted_property.values["default"]
else:
result[deleted_property.id] = None
self.__extraPropertyList = new_properties
ZMIField._edit(self, result)
security.declareProtected('Access contents information', 'get_value')
def get_value(self, id, **kw):
if self.values.has_key(id):
return self.values[id]
return ZMIField.get_value(self, id, **kw)
def getCaptchaCustomPropertyList(self):
captcha_type = self.get_value("captcha_type")
captcha_provider = CaptchaProviderFactory.getProvider(captcha_type)
extraPropertyList = captcha_provider.getExtraPropertyList()
self.__extraPropertyList = [x.id for x in extraPropertyList]
return extraPropertyList
\ No newline at end of file
# -*- coding: utf-8 -*-
#---------------------------------------------------------------------
# Python module for easy utilization of http://captchas.net
#
# For documentation look at http://captchas.net/sample/python/
#
# Written by Sebastian Wilhelmi <seppi@seppi.de> and
# Felix Holderied <felix@holderied.de>
# This file is in the public domain.
#
# ChangeLog:
#
# 2010-01-15: Adapt to ERP5 : a lot of code had to be removed or changed.
# Most of the work must be done in another class.
#
# 2006-09-08: Add new optional parameters alphabet, letters
# height an width. Add audio_url.
#
# 2006-03-01: Only delete the random string from the repository in
# case of a successful verification.
#
# 2006-02-14: Add new image() method returning an HTML/JavaScript
# snippet providing a fault tolerant service.
#
# 2005-06-02: Initial version.
#
#---------------------------------------------------------------------
import md5
import random
class CaptchasDotNet:
def __init__ (self, client, secret,
alphabet = 'abcdefghkmnopqrstuvwxyz',
letters = 6,
width = 240,
height = 80
):
self.__client = client
self.__secret = secret
self.__alphabet = alphabet
self.__letters = letters
self.__width = width
self.__height = height
# Return a random string
def random_string (self):
# The random string shall consist of small letters, big letters
# and digits.
letters = "abcdefghijklmnopqrstuvwxyz"
letters += letters.upper () + "0123456789"
# The random starts out empty, then 40 random possible characters
# are appended.
random_string = ''
for i in range (40):
random_string += random.choice (letters)
# Return the random string.
return random_string
def image_url (self, random, base = 'http://image.captchas.net/'):
url = base
url += '?client=%s&amp;random=%s' % (self.__client, random)
if self.__alphabet != "abcdefghijklmnopqrstuvwxyz":
url += '&amp;alphabet=%s' % self.__alphabet
if self.__letters != 6:
url += '&amp;letters=%s' % self.__letters
if self.__width != 240:
url += '&amp;width=%s' % self.__width
if self.__height != 80:
url += '&amp;height=%s' % self.__height
return url
def audio_url (self, random, base = 'http://audio.captchas.net/'):
url = base
url += '?client=%s&amp;random=%s' % (self.__client, random)
if self.__alphabet != "abcdefghijklmnopqrstuvwxyz":
url += '&amp;alphabet=%s' % self.__alphabet
if self.__letters != 6:
url += '&amp;letters=%s' % self.__letters
return url
def image (self, random, id = 'captchas.net'):
return '''
<a href="http://captchas.net"><img
style="border: none; vertical-align: bottom"
id="%s" src="%s" width="%d" height="%d"
alt="The CAPTCHA image" /></a>
<script type="text/javascript">
<!--
function captchas_image_error (image)
{
if (!image.timeout) return true;
image.src = image.src.replace (/^http:\/\/image\.captchas\.net/,
'http://image.backup.captchas.net');
return captchas_image_loaded (image);
}
function captchas_image_loaded (image)
{
if (!image.timeout) return true;
window.clearTimeout (image.timeout);
image.timeout = false;
return true;
}
var image = document.getElementById ('%s');
image.onerror = function() {return captchas_image_error (image);};
image.onload = function() {return captchas_image_loaded (image);};
image.timeout
= window.setTimeout(
"captchas_image_error (document.getElementById ('%s'))",
10000);
image.src = image.src;
//-->
</script>''' % (id, self.image_url (random), self.__width, self.__height, id, id)
def get_answer (self, random ):
# The format of the password.
password_alphabet = self.__alphabet
password_length = self.__letters
# Calculate the MD5 digest of the concatenation of secret key and
# random string.
encryption_base = self.__secret + random
if (password_alphabet != "abcdefghijklmnopqrstuvwxyz") or (password_length != 6):
encryption_base += ":" + password_alphabet + ":" + str(password_length)
digest = md5.new (encryption_base).digest ()
# Compute password
correct_password = ''
for pos in range (password_length):
letter_num = ord (digest[pos]) % len (password_alphabet)
correct_password += password_alphabet[letter_num]
return correct_password
\ No newline at end of file
...@@ -45,6 +45,7 @@ from Tool import SelectionTool ...@@ -45,6 +45,7 @@ from Tool import SelectionTool
import OOoChart, PDFTemplate, Report, PDFForm, ParallelListField import OOoChart, PDFTemplate, Report, PDFForm, ParallelListField
import PlanningBox, POSBox, FormBox, EditorField, ProxyField, DurationField import PlanningBox, POSBox, FormBox, EditorField, ProxyField, DurationField
import RelationField, ImageField, MultiRelationField, MultiLinkField, InputButtonField import RelationField, ImageField, MultiRelationField, MultiLinkField, InputButtonField
import CaptchaField
import PreferenceTool import PreferenceTool
from Products.Formulator.FieldRegistry import FieldRegistry from Products.Formulator.FieldRegistry import FieldRegistry
...@@ -143,7 +144,9 @@ def initialize( context ): ...@@ -143,7 +144,9 @@ def initialize( context ):
'www/StringField.gif') 'www/StringField.gif')
FieldRegistry.registerField(OOoChart.OOoChart, FieldRegistry.registerField(OOoChart.OOoChart,
'www/StringField.gif') 'www/StringField.gif')
FieldRegistry.registerField(CaptchaField.CaptchaField,
'www/StringField.gif')
# some helper fields # some helper fields
FieldRegistry.registerField(HelperFields.ListTextAreaField) FieldRegistry.registerField(HelperFields.ListTextAreaField)
FieldRegistry.registerField(HelperFields.MethodField) FieldRegistry.registerField(HelperFields.MethodField)
......
<dtml-var manage_page_header>
<dtml-let help_product="'Formulator'" help_topic=meta_type>
<dtml-var manage_tabs>
</dtml-let>
<p class="form-help">
Surcharge <dtml-var meta_type> properties here.
</p>
<form action="manage_edit" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<!-- First, display normal properties -->
<!-- see: Formulator/dtml/fieldEdit.dtml -->
<dtml-in "form.get_groups()">
<dtml-let group=sequence-item fields="form.get_fields_in_group(group)">
<dtml-if fields>
<tr>
<td colspan="3" class="form-title">
Captcha Widget properties
</td>
</tr>
<dtml-var fieldListHeader>
<dtml-let current_field="this()">
<dtml-in fields>
<dtml-let field=sequence-item field_id="field.id"
value="current_field.get_orig_value(field_id)"
override="current_field.get_override(field_id)"
tales="current_field.get_tales(field_id)">
<tr>
<td align="left" valign="top">
<div class="form-label">
<dtml-if "tales or override">[</dtml-if><dtml-var "field.title()"><dtml-if "field.has_value('required') and field.get_value('required')">*</dtml-if><dtml-if "tales or override">]</dtml-if>
</div>
</td>
<td align="left" valign="top">
<dtml-var "field.render(value)">
</td>
<td><div class="form-element">
<dtml-var "field.meta_type">
</div></td>
</tr>
</dtml-let>
</dtml-in>
</dtml-let>
</dtml-if>
</dtml-let>
</dtml-in>
<!-- Then, display captcha-specific properties -->
<dtml-let current_field="this()">
<dtml-in "this().getCaptchaCustomPropertyList()" prefix="captcha">
<dtml-var expr="captcha_item">
<dtml-let field="captcha_item.get_real_field()" field_id="field.id"
value="current_field.get_orig_value(field_id)"
override="current_field.get_override(field_id)"
tales="current_field.get_tales(field_id)">
<tr>
<td align="left" valign="top">
<div class="form-label">
<dtml-if "tales or override">[</dtml-if><dtml-var "field.title()"><dtml-if "field.has_value('required') and field.get_value('required')">*</dtml-if><dtml-if "tales or override">]</dtml-if>
</div>
</td>
<td align="left" valign="top">
<dtml-var "field.render(value)">
</td>
<td><div class="form-element">
<dtml-var "field.meta_type">
</div></td>
</tr>
</dtml-let>
</dtml-in>
</dtml-let>
<tr>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value="Save Changes" />
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment