Updated form's captcha and proxy management by adding default settings to forms manager
authorThierry Florac <tflorac@ulthar.net>
Wed, 16 Oct 2019 18:41:45 +0200
changeset 1370 87bcbf37ad6d
parent 1369 3f206017a2c0
child 1371 865291f1616d
Updated form's captcha and proxy management by adding default settings to forms manager
src/pyams_content/shared/form/__init__.py
src/pyams_content/shared/form/interfaces.py
src/pyams_content/shared/form/manager.py
src/pyams_content/shared/form/zmi/manager.py
src/pyams_content/shared/form/zmi/properties.py
--- a/src/pyams_content/shared/form/__init__.py	Mon Oct 07 14:04:31 2019 +0200
+++ b/src/pyams_content/shared/form/__init__.py	Wed Oct 16 18:41:45 2019 +0200
@@ -19,10 +19,12 @@
 from pyams_content.features.review.interfaces import IReviewTarget
 from pyams_content.shared.common import IWfSharedContentFactory, SharedContent, WfSharedContent, WfSharedContentChecker, \
     register_content_type
-from pyams_content.shared.form.interfaces import FORM_CONTENT_NAME, FORM_CONTENT_TYPE, IForm, IFormFieldContainer, \
-    IFormFieldContainerTarget, IFormHandler, IWfForm, IWfFormFactory
+from pyams_content.shared.form.interfaces import FORM_CONTENT_NAME, FORM_CONTENT_TYPE, IForm, \
+    IFormFieldContainer, \
+    IFormFieldContainerTarget, IFormHandler, IWfForm, IWfFormFactory, IFormsManager
 from pyams_utils.adapter import adapter_config
 from pyams_utils.registry import get_global_registry
+from pyams_utils.traversing import get_parent
 
 
 __docformat__ = 'restructuredtext'
@@ -45,10 +47,9 @@
     submit_label = FieldProperty(IWfForm['submit_label'])
     submit_message = FieldProperty(IWfForm['submit_message'])
     _handler = FieldProperty(IWfForm['handler'])
-    use_captcha = FieldProperty(IWfForm['use_captcha'])
+    override_captcha = FieldProperty(IWfForm['override_captcha'])
     client_captcha_key = FieldProperty(IWfForm['client_captcha_key'])
     server_captcha_key = FieldProperty(IWfForm['server_captcha_key'])
-    captcha_proxy = FieldProperty(IWfForm['captcha_proxy'])
     rgpd_consent = FieldProperty(IWfForm['rgpd_consent'])
     rgpd_warning = FieldProperty(IWfForm['rgpd_warning'])
     rgpd_user_rights = FieldProperty(IWfForm['rgpd_user_rights'])
@@ -86,6 +87,17 @@
             registry = get_global_registry()
             return registry.queryUtility(IFormHandler, name=handler)
 
+    def get_captcha_settings(self):
+        if self.override_captcha:
+            return {
+                'use_captcha': bool(self.client_captcha_key),
+                'client_key': self.client_captcha_key,
+                'server_key': self.server_captcha_key
+            }
+        else:
+            manager = get_parent(self, IFormsManager)
+            return manager.get_captcha_settings()
+
 
 register_content_type(WfForm, shared_content=False)
 
--- a/src/pyams_content/shared/form/interfaces.py	Mon Oct 07 14:04:31 2019 +0200
+++ b/src/pyams_content/shared/form/interfaces.py	Wed Oct 16 18:41:45 2019 +0200
@@ -13,8 +13,9 @@
 from zope.annotation.interfaces import IAttributeAnnotatable
 from zope.container.constraints import containers, contains
 from zope.container.interfaces import IContained, IContainer
-from zope.interface import Attribute, Interface
-from zope.schema import Bool, Choice, TextLine
+from zope.interface import Attribute, Interface, invariant
+from zope.interface.interfaces import Invalid
+from zope.schema import Bool, Choice, TextLine, Int, Password
 
 from pyams_content.component.paragraph import IBaseParagraph
 from pyams_content.shared.common.interfaces import ISharedContent, ISharedToolPortalContext, \
@@ -37,6 +38,67 @@
 class IFormsManager(ISharedToolPortalContext):
     """Forms manager interface"""
 
+    use_captcha = Bool(title=_("Use captcha?"),
+                       description=_("Set default captcha settings"),
+                       required=True,
+                       default=False)
+
+    default_captcha_client_key = TextLine(title=_("Default captcha site key"),
+                                          description=_("This key is included into HTML code and "
+                                                        "submitted with form data"),
+                                          required=False)
+
+    default_captcha_server_key = TextLine(title=_("Default captcha secret key"),
+                                          description=_("This key is used to communicate with "
+                                                        "Google's reCaptcha services"),
+                                          required=False)
+
+    def get_captcha_settings(self):
+        """Get default captcha settings"""
+
+    use_proxy = Bool(title=_("Use proxy server?"),
+                     description=_("If a proxy server is required to access recaptcha services, "
+                                   "please set them here"),
+                     required=True,
+                     default=False)
+
+    proxy_proto = Choice(title=_("Protocol"),
+                         description=_("If your server is behind a proxy, please set it's "
+                                       "protocol here; HTTPS support is required for reCaptcha"),
+                         required=False,
+                         values=('http', 'https'),
+                         default='http')
+
+    proxy_host = TextLine(title=_("Host name"),
+                          description=_("If your server is behind a proxy, please set it's "
+                                        "address here; captcha verification requires HTTPS "
+                                        "support..."),
+                          required=False)
+
+    proxy_port = Int(title=_("Port number"),
+                     description=_("If your server is behind a proxy, plase set it's port "
+                                   "number here"),
+                     required=False,
+                     default=8080)
+
+    proxy_username = TextLine(title=_("Username"),
+                              description=_("If your proxy server requires authentication, "
+                                            "please set username here"),
+                              required=False)
+
+    proxy_password = Password(title=_("Password"),
+                              description=_("If your proxy server requires authentication, "
+                                            "please set password here"),
+                              required=False)
+
+    proxy_only_from = TextLine(title=_("Use proxy only from"),
+                               description=_("If proxy usage is restricted to several domains "
+                                             "names, you can set them here, separated by comas"),
+                               required=False)
+
+    def get_proxy_url(self, request):
+        """Get proxy server URL"""
+
 
 class IFormsManagerFactory(Interface):
     """Forms manager factory interface"""
@@ -175,10 +237,10 @@
                      description=_("Select how form data is transmitted"),
                      vocabulary='PyAMS form handlers')
 
-    use_captcha = Bool(title=_("Use captcha?"),
-                       description=_("If 'yes', a captcha will be added automatically to the form"),
-                       required=False,
-                       default=True)
+    override_captcha = Bool(title=_("Override captcha settings?"),
+                            description=_("If 'yes', you can define custom captcha keys here"),
+                            required=False,
+                            default=True)
 
     client_captcha_key = TextLine(title=_("Site key"),
                                   description=_("This key is included into HTML code and submitted "
@@ -190,11 +252,8 @@
                                                 "reCaptcha services"),
                                   required=False)
 
-    captcha_proxy = TextLine(title=_("Recaptcha proxy"),
-                             description=_("If your server is behind a proxy, please set it's "
-                                           "address here; captcha verification requires HTTPS "
-                                           "support..."),
-                             required=False)
+    def get_captcha_settings(self):
+        """Get form captcha settings"""
 
     rgpd_consent = Bool(title=_("Required RGPD consent?"),
                         description=_("If 'yes', an RGPD compliance warning will be displayed "
--- a/src/pyams_content/shared/form/manager.py	Mon Oct 07 14:04:31 2019 +0200
+++ b/src/pyams_content/shared/form/manager.py	Wed Oct 16 18:41:45 2019 +0200
@@ -14,6 +14,7 @@
 from zope.component.interfaces import ISite
 from zope.interface import implementer
 from zope.lifecycleevent.interfaces import IObjectAddedEvent
+from zope.schema.fieldproperty import FieldProperty
 
 from pyams_content.component.paragraph import IParagraphFactorySettings
 from pyams_content.shared.common.interfaces import ISharedContentFactory
@@ -36,6 +37,46 @@
     shared_content_type = FORM_CONTENT_TYPE
     shared_content_menu = False
 
+    use_captcha = FieldProperty(IFormsManager['use_captcha'])
+    default_captcha_client_key = FieldProperty(IFormsManager['default_captcha_client_key'])
+    default_captcha_server_key = FieldProperty(IFormsManager['default_captcha_server_key'])
+    use_proxy = FieldProperty(IFormsManager['use_proxy'])
+    proxy_proto = FieldProperty(IFormsManager['proxy_proto'])
+    proxy_host = FieldProperty(IFormsManager['proxy_host'])
+    proxy_port = FieldProperty(IFormsManager['proxy_port'])
+    proxy_username = FieldProperty(IFormsManager['proxy_username'])
+    proxy_password = FieldProperty(IFormsManager['proxy_password'])
+    proxy_only_from = FieldProperty(IFormsManager['proxy_only_from'])
+
+    def get_captcha_settings(self):
+        result = {
+            'use_captcha': False,
+            'client_key': None,
+            'server_key': None
+        }
+        if self.use_captcha:
+            result.update({
+                'use_captcha': True,
+                'client_key': self.default_captcha_client_key,
+                'server_key': self.default_captcha_server_key
+            })
+        return result
+
+    def get_proxy_url(self, request):
+        if self.use_proxy:
+            # check selected domains names
+            if self.proxy_only_from:
+                domains = map(str.strip, self.proxy_only_from.split(','))
+                if request.host not in domains:
+                    return None
+            return '{}://{}{}:{}/'.format(self.proxy_proto,
+                                          '{}{}{}@'.format(self.proxy_username,
+                                                           ':' if self.proxy_password else '',
+                                                           self.proxy_password or '')
+                                          if self.proxy_username else '',
+                                          self.proxy_host,
+                                          self.proxy_port)
+
 
 @utility_config(provides=IFormsManagerFactory)
 class FormsManagerFactory(object):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/form/zmi/manager.py	Wed Oct 16 18:41:45 2019 +0200
@@ -0,0 +1,91 @@
+#
+# Copyright (c) 2008-2019 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+from pyramid.events import subscriber
+from z3c.form import field
+from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget
+from z3c.form.interfaces import IDataExtractedEvent
+from zope.interface import Invalid
+
+from pyams_content.shared.common.zmi.manager import SharedToolPropertiesEditForm
+from pyams_content.shared.form.interfaces import IFormsManager
+from pyams_form.group import NamedWidgetsGroup
+from pyams_form.interfaces.form import IInnerSubForm
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.adapter import adapter_config
+from pyams_zmi.form import InnerAdminEditForm
+
+
+__docformat__ = 'restructuredtext'
+
+from pyams_content import _
+
+
+@adapter_config(name='captcha-settings',
+                context=(IFormsManager, IPyAMSLayer, SharedToolPropertiesEditForm),
+                provides=IInnerSubForm)
+class FormManagerCaptchaSettingsEditForm(InnerAdminEditForm):
+    """Form manager captcha settings edit form"""
+
+    prefix = 'captcha_properties.'
+
+    legend = _("Captcha settings")
+    fieldset_class = 'bordered no-x-margin margin-y-10'
+
+    fields = field.Fields(IFormsManager).select('use_captcha',
+                                                'default_captcha_client_key',
+                                                'default_captcha_server_key',
+                                                'use_proxy',
+                                                'proxy_proto',
+                                                'proxy_host',
+                                                'proxy_port',
+                                                'proxy_username',
+                                                'proxy_password',
+                                                'proxy_only_from')
+    fields['use_captcha'].widgetFactory = SingleCheckBoxFieldWidget
+    fields['use_proxy'].widgetFactory = SingleCheckBoxFieldWidget
+
+    weight = 1
+
+    def updateGroups(self):
+        self.add_group(NamedWidgetsGroup(self, 'captcha', self.widgets,
+                                         ('use_captcha', 'default_captcha_client_key',
+                                          'default_captcha_server_key'),
+                                         fieldset_class='inner bordered',
+                                         legend=_("Use captcha"),
+                                         css_class='inner',
+                                         switch=True,
+                                         checkbox_switch=True,
+                                         checkbox_field=IFormsManager['use_captcha']))
+        self.add_group(NamedWidgetsGroup(self, 'proxy', self.widgets,
+                                         ('use_proxy', 'proxy_proto', 'proxy_host',
+                                          'proxy_port', 'proxy_username', 'proxy_password',
+                                          'proxy_only_from'),
+                                         fieldset_class='inner bordered',
+                                         legend=_("Use proxy server"),
+                                         css_class='inner',
+                                         switch=True,
+                                         checkbox_switch=True,
+                                         checkbox_field=IFormsManager['use_proxy']))
+        super(FormManagerCaptchaSettingsEditForm, self).updateGroups()
+
+
+@subscriber(IDataExtractedEvent, form_selector=FormManagerCaptchaSettingsEditForm)
+def check_form_captcha_data(event):
+    """Check captcha form input data"""
+    data = event.data
+    if data.get('use_captcha') and not (data.get('default_captcha_client_key') and
+                                        data.get('default_captcha_server_key')):
+        event.form.widgets.errors += (Invalid(_("You must define client and server key to "
+                                                "activate a captcha")), )
+    if data.get('use_proxy') and not data.get('proxy_host'):
+        event.form.widgets.errors += (Invalid(_("You must define hostname to use a proxy")), )
--- a/src/pyams_content/shared/form/zmi/properties.py	Mon Oct 07 14:04:31 2019 +0200
+++ b/src/pyams_content/shared/form/zmi/properties.py	Wed Oct 16 18:41:45 2019 +0200
@@ -58,10 +58,10 @@
 
     fields = field.Fields(IWfForm).select('form_header', 'user_title', 'auth_only',
                                           'submit_label', 'submit_message', 'handler',
-                                          'use_captcha', 'client_captcha_key',
-                                          'server_captcha_key', 'captcha_proxy',
+                                          'override_captcha', 'client_captcha_key',
+                                          'server_captcha_key',
                                           'rgpd_consent', 'rgpd_warning', 'rgpd_user_rights')
-    fields['use_captcha'].widgetFactory = SingleCheckBoxFieldWidget
+    fields['override_captcha'].widgetFactory = SingleCheckBoxFieldWidget
     fields['rgpd_consent'].widgetFactory = SingleCheckBoxFieldWidget
 
     weight = 1
@@ -96,14 +96,14 @@
                                          ('form_header', 'user_title', 'auth_only',
                                           'submit_label', 'submit_message', 'handler')))
         self.add_group(NamedWidgetsGroup(self, 'captcha', self.widgets,
-                                         ('use_captcha', 'client_captcha_key',
-                                          'server_captcha_key', 'captcha_proxy'),
+                                         ('override_captcha', 'client_captcha_key',
+                                          'server_captcha_key'),
                                          fieldset_class='inner bordered',
-                                         legend=_("Add captcha"),
+                                         legend=_("Override default captcha settings"),
                                          css_class='inner',
                                          switch=True,
                                          checkbox_switch=True,
-                                         checkbox_field=IWfForm['use_captcha']))
+                                         checkbox_field=IWfForm['override_captcha']))
         self.add_group(NamedWidgetsGroup(self, 'rgpd', self.widgets,
                                          ('rgpd_consent', 'rgpd_warning', 'rgpd_user_rights'),
                                          fieldset_class='inner bordered',