[Erp5-report] r32007 aurel - in /erp5/trunk/products/ERP5Form: ./ dtml/
nobody at svn.erp5.org
nobody at svn.erp5.org
Wed Jan 27 10:07:16 CET 2010
Author: aurel
Date: Wed Jan 27 10:07:12 2010
New Revision: 32007
URL: http://svn.erp5.org?rev=32007&view=rev
Log:
add Captcha field, work done by Pierre
Added:
erp5/trunk/products/ERP5Form/CaptchaField.py
erp5/trunk/products/ERP5Form/CaptchasDotNet.py
erp5/trunk/products/ERP5Form/dtml/captchaFieldEdit.dtml
Modified:
erp5/trunk/products/ERP5Form/__init__.py
Added: erp5/trunk/products/ERP5Form/CaptchaField.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Form/CaptchaField.py?rev=32007&view=auto
==============================================================================
--- erp5/trunk/products/ERP5Form/CaptchaField.py (added)
+++ erp5/trunk/products/ERP5Form/CaptchaField.py [utf8] Wed Jan 27 10:07:12 2010
@@ -1,0 +1,325 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2010 Nexedi SARL and Contributors. All Rights Reserved.
+# Pierre Ducroquet <pierre.ducroquet at 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
+
Added: erp5/trunk/products/ERP5Form/CaptchasDotNet.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Form/CaptchasDotNet.py?rev=32007&view=auto
==============================================================================
--- erp5/trunk/products/ERP5Form/CaptchasDotNet.py (added)
+++ erp5/trunk/products/ERP5Form/CaptchasDotNet.py [utf8] Wed Jan 27 10:07:12 2010
@@ -1,0 +1,137 @@
+# -*- 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 at seppi.de> and
+# Felix Holderied <felix at 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&random=%s' % (self.__client, random)
+ if self.__alphabet != "abcdefghijklmnopqrstuvwxyz":
+ url += '&alphabet=%s' % self.__alphabet
+ if self.__letters != 6:
+ url += '&letters=%s' % self.__letters
+ if self.__width != 240:
+ url += '&width=%s' % self.__width
+ if self.__height != 80:
+ url += '&height=%s' % self.__height
+ return url
+
+ def audio_url (self, random, base = 'http://audio.captchas.net/'):
+ url = base
+ url += '?client=%s&random=%s' % (self.__client, random)
+ if self.__alphabet != "abcdefghijklmnopqrstuvwxyz":
+ url += '&alphabet=%s' % self.__alphabet
+ if self.__letters != 6:
+ url += '&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
Modified: erp5/trunk/products/ERP5Form/__init__.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Form/__init__.py?rev=32007&r1=32006&r2=32007&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Form/__init__.py [utf8] (original)
+++ erp5/trunk/products/ERP5Form/__init__.py [utf8] Wed Jan 27 10:07:12 2010
@@ -45,6 +45,7 @@
import OOoChart, PDFTemplate, Report, PDFForm, ParallelListField
import PlanningBox, POSBox, FormBox, EditorField, ProxyField, DurationField
import RelationField, ImageField, MultiRelationField, MultiLinkField, InputButtonField
+import CaptchaField
import PreferenceTool
from Products.Formulator.FieldRegistry import FieldRegistry
@@ -143,7 +144,9 @@
'www/StringField.gif')
FieldRegistry.registerField(OOoChart.OOoChart,
'www/StringField.gif')
-
+ FieldRegistry.registerField(CaptchaField.CaptchaField,
+ 'www/StringField.gif')
+
# some helper fields
FieldRegistry.registerField(HelperFields.ListTextAreaField)
FieldRegistry.registerField(HelperFields.MethodField)
Added: erp5/trunk/products/ERP5Form/dtml/captchaFieldEdit.dtml
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Form/dtml/captchaFieldEdit.dtml?rev=32007&view=auto
==============================================================================
--- erp5/trunk/products/ERP5Form/dtml/captchaFieldEdit.dtml (added)
+++ erp5/trunk/products/ERP5Form/dtml/captchaFieldEdit.dtml [utf8] Wed Jan 27 10:07:12 2010
@@ -1,0 +1,98 @@
+<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>
More information about the Erp5-report
mailing list