From a6e9502145995d48ca0482a23a4b0d48027e4753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Calonne?= <aurel@nexedi.com> Date: Wed, 27 Jan 2010 09:07:12 +0000 Subject: [PATCH] add Captcha field, work done by Pierre git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@32007 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5Form/CaptchaField.py | 325 ++++++++++++++++++++ product/ERP5Form/CaptchasDotNet.py | 137 +++++++++ product/ERP5Form/__init__.py | 5 +- product/ERP5Form/dtml/captchaFieldEdit.dtml | 98 ++++++ 4 files changed, 564 insertions(+), 1 deletion(-) create mode 100644 product/ERP5Form/CaptchaField.py create mode 100644 product/ERP5Form/CaptchasDotNet.py create mode 100644 product/ERP5Form/dtml/captchaFieldEdit.dtml diff --git a/product/ERP5Form/CaptchaField.py b/product/ERP5Form/CaptchaField.py new file mode 100644 index 0000000000..50c5ba3c4a --- /dev/null +++ b/product/ERP5Form/CaptchaField.py @@ -0,0 +1,325 @@ +# -*- 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 diff --git a/product/ERP5Form/CaptchasDotNet.py b/product/ERP5Form/CaptchasDotNet.py new file mode 100644 index 0000000000..6e2c060be4 --- /dev/null +++ b/product/ERP5Form/CaptchasDotNet.py @@ -0,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@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&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 \ No newline at end of file diff --git a/product/ERP5Form/__init__.py b/product/ERP5Form/__init__.py index ee816c297e..45042c69b5 100644 --- a/product/ERP5Form/__init__.py +++ b/product/ERP5Form/__init__.py @@ -45,6 +45,7 @@ from Tool import SelectionTool 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 @@ def initialize( context ): '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) diff --git a/product/ERP5Form/dtml/captchaFieldEdit.dtml b/product/ERP5Form/dtml/captchaFieldEdit.dtml new file mode 100644 index 0000000000..8bd012e761 --- /dev/null +++ b/product/ERP5Form/dtml/captchaFieldEdit.dtml @@ -0,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> -- 2.30.9