# -*- 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
from Products.Formulator.TALESField import TALESField
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"
    captchas_use_ssl = field.get_value("captcha_dot_net_use_ssl") or False
    return CaptchasDotNet.CaptchasDotNet(client = captchas_client, 
                                        secret = captchas_secret, 
                                        use_ssl = captchas_use_ssl)
  
  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())

  # dynamic fields
  _dynamic_property_list = [dict(id='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),
                            dict(id='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),
                            dict(id='captcha_dot_net_use_ssl',
                                 title='Captchas.net ssl connection',
                                 description='Use secured connection with the service',
                                 default=0,                                 
                                 required=0)]

  def getExtraPropertyList(self):
    return [fields.StringField(**self._dynamic_property_list[0]),
            fields.PasswordField(**self._dynamic_property_list[1]),
            fields.CheckBoxField(**self._dynamic_property_list[2])]    

  def getExtraTalesPropertyList(self):
    return [TALESField(**self._dynamic_property_list[0]),
            TALESField(**self._dynamic_property_list[1]),
            TALESField(**self._dynamic_property_list[2])]                          

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 []

  def getExtraTalesPropertyList(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 add_captcha(self, portal_sessions, key, value):
    session = portal_sessions[key]
    if session.has_key(key):
      return False
    session[key] = value
    return True    
    
  def validate_answer(self, portal_sessions, key, value):
    session = portal_sessions[key]
    if not(session.has_key(key)):
      return False
    result = (session[key] == value)
    # Forbid several use of the same captcha.
    del(session[key])
    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)
    portal_sessions = field.getPortalObject().portal_sessions  
    while not(self.add_captcha(portal_sessions, 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 + "__")
    portal_sessions = field.getPortalObject().portal_sessions
    if not(CaptchaWidgetInstance.validate_answer(portal_sessions, 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 id in self.getCaptchaCustomPropertyList():
      return self.values[id]
    return ZMIField.get_value(self, id, **kw)

  def getCaptchaCustomPropertyList(self):
    if hasattr(self, "__extraPropertyList"):
      return self.__extraPropertyList
    captcha_type = ZMIField.get_value(self, "captcha_type")
    captcha_provider = CaptchaProviderFactory.getProvider(captcha_type)
    extraPropertyList = captcha_provider.getExtraPropertyList()
    self.__extraPropertyList = [x.id for x in extraPropertyList]
    return extraPropertyList



  security.declareProtected('View management screens', 'manage_talesForm')
  manage_talesForm = DTMLFile('dtml/captchaFieldTales', globals())

  security.declareProtected('Change Formulator Forms', 'manage_tales')
  def manage_tales(self, REQUEST):
    """Change TALES expressions.
    """
    result = {}
    # add dynamic form fields for captcha
    #captcha_provider = CaptchaProviderFactory.getProvider(self.get_value("captcha_type"))
    for field in self.getCaptchaCustomTalesPropertyList():
      try:
        # validate the form and get results
        result[field.id] = field.validate(REQUEST)
      except ValidationError, err:
        if REQUEST:
          message = "Error: %s - %s" % (err.field.get_value('title'),
                                        err.error_text)
          return self.manage_talesForm(self, REQUEST,
                                  manage_tabs_message=message)
        else:
          raise
      
    # standard tales form fields
    try:
      # validate the form and get results
      result.update(self.tales_form.validate(REQUEST))
    except ValidationError, err:
      if REQUEST:
        message = "Error: %s - %s" % (err.field.get_value('title'),
                                      err.error_text)
        return self.manage_talesForm(self,REQUEST,
                                     manage_tabs_message=message)
      else:
        raise
    self._edit_tales(result)

    if REQUEST:
      message="Content changed."
      return self.manage_talesForm(self, REQUEST,
                                   manage_tabs_message=message)
                                     
  def getCaptchaCustomTalesPropertyList(self):
    if hasattr(self, "__extraTalesPropertyList"):
      return self.__extraTalesPropertyList
    captcha_type = ZMIField.get_value(self, "captcha_type")
    captcha_provider = CaptchaProviderFactory.getProvider(captcha_type)
    extraPropertyList = captcha_provider.getExtraTalesPropertyList()
    self.__extraTalesPropertyList = [x.id for x in extraPropertyList]
    return extraPropertyList                                     
  