wagtail-enap-designsystem 1.2.1.138__py3-none-any.whl → 1.2.1.140__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,7 @@
1
1
  from django.db import models
2
+ from django import forms
3
+ from wagtail.admin.forms import WagtailAdminPageForm
4
+ from .security import validate_safe_characters, validate_email_field
2
5
  from django.http import JsonResponse
3
6
  from django.shortcuts import render, redirect
4
7
  from django.core.mail import send_mail
@@ -26,7 +29,7 @@ from wagtail.blocks import StreamBlock, StructBlock, CharBlock
26
29
  from ..utils.services import SimpleEmailService
27
30
  logger = logging.getLogger(__name__)
28
31
 
29
- # models.py
32
+
30
33
  from django.db import models
31
34
  from wagtail.models import Page
32
35
  from wagtail.fields import RichTextField, StreamField
@@ -34,6 +37,37 @@ from wagtail.admin.panels import FieldPanel, MultiFieldPanel, InlinePanel
34
37
  from wagtail import blocks
35
38
  from wagtail.images.blocks import ImageChooserBlock
36
39
  from wagtail.blocks import StreamBlock, StructBlock, CharBlock, RichTextBlock, URLBlock
40
+
41
+
42
+
43
+
44
+ class SafeFormMixin:
45
+ """
46
+ Aplica validação de caracteres em todos os campos
47
+ """
48
+ def __init__(self, *args, **kwargs):
49
+ super().__init__(*args, **kwargs)
50
+ self.apply_character_validation()
51
+
52
+ def apply_character_validation(self):
53
+ """
54
+ Aplica validação baseada no tipo de campo
55
+ """
56
+ for field_name, field in self.fields.items():
57
+ if isinstance(field, forms.EmailField):
58
+ # Para emails, permitir @ e .
59
+ field.validators.append(validate_email_field)
60
+ elif isinstance(field, (forms.CharField, forms.TextField)):
61
+ # Para texto comum, validação padrão
62
+ field.validators.append(validate_safe_characters)
63
+
64
+ class FormularioPageForm(SafeFormMixin, WagtailAdminPageForm):
65
+ pass
66
+
67
+
68
+
69
+
70
+
37
71
  class TextFieldBlock(blocks.StructBlock):
38
72
  """Campo de texto simples"""
39
73
  label = blocks.CharBlock(label="Rótulo", max_length=255)
@@ -1913,7 +1947,6 @@ class FormularioPage(Page):
1913
1947
 
1914
1948
  return extracted_fields
1915
1949
 
1916
- print(f"=== PROCESSANDO {len(self.form_steps)} STEPS (Versão Corrigida) ===")
1917
1950
 
1918
1951
  # Processar cada step
1919
1952
  for index, step_block in enumerate(self.form_steps):
@@ -1937,7 +1970,6 @@ class FormularioPage(Page):
1937
1970
  'sections': []
1938
1971
  }
1939
1972
 
1940
- print(f"✅ Step {index + 1}: {len(step_fields)} campos (sem aninhamento)")
1941
1973
 
1942
1974
  # Organizar em seções
1943
1975
  current_section = None
@@ -1962,7 +1994,6 @@ class FormularioPage(Page):
1962
1994
  steps.append(step_data)
1963
1995
 
1964
1996
  total_fields = sum(len(step['fields']) for step in steps)
1965
- print(f"🎉 RESULTADO: {len(steps)} steps com {total_fields} campos principais")
1966
1997
 
1967
1998
  return steps
1968
1999
 
@@ -2069,6 +2100,59 @@ class FormularioPage(Page):
2069
2100
  context['admin_notified'] = request.GET.get('admin_notified') == '1'
2070
2101
 
2071
2102
  return context
2103
+
2104
+
2105
+ def validate_form_data(self, form_data, request):
2106
+ """Valida os dados do formulário - VERSÃO COM PROTEÇÃO DE SEGURANÇA"""
2107
+ errors = {}
2108
+
2109
+ for step in self.get_all_steps():
2110
+ for block in step['fields']:
2111
+ if block.block_type in ['info_text', 'divider', 'section_header']:
2112
+ continue
2113
+
2114
+ field_id = f"{block.block_type}_{block.id}"
2115
+ value = form_data.get(field_id, '')
2116
+
2117
+ # Verificar se campo condicional deve ser validado
2118
+ if block.block_type in ['city_field', 'conditional_dropdown_field']:
2119
+ if not self.should_process_conditional_field(block, form_data, request):
2120
+ continue
2121
+
2122
+ # NOVA VALIDAÇÃO DE SEGURANÇA - APLICAR A TODOS OS CAMPOS DE TEXTO
2123
+ if isinstance(value, str) and value.strip():
2124
+ try:
2125
+ if block.block_type == 'email_field':
2126
+ # Para emails, usar validador específico
2127
+ validate_email_field(value)
2128
+ else:
2129
+ # Para outros campos de texto, usar validador geral
2130
+ validate_safe_characters(value)
2131
+ except ValidationError as e:
2132
+ errors[field_id] = str(e.message)
2133
+ continue
2134
+
2135
+ # Verificar campos obrigatórios
2136
+ if block.value.get('required', False):
2137
+ if not value or (isinstance(value, list) and not any(value)):
2138
+ errors[field_id] = 'Este campo é obrigatório'
2139
+ continue
2140
+
2141
+ # Validações específicas existentes
2142
+ if block.block_type == 'email_field' and value:
2143
+ if not self.validate_email(value):
2144
+ errors[field_id] = 'Email inválido'
2145
+
2146
+ elif block.block_type == 'cpf_field' and value:
2147
+ if not self.validate_cpf_9_digits(value):
2148
+ errors[field_id] = 'CPF deve ter exatamente 11 dígitos'
2149
+
2150
+ elif block.block_type == 'phone_field' and value:
2151
+ if not self.validate_phone(value):
2152
+ errors[field_id] = 'Telefone inválido'
2153
+
2154
+ return errors
2155
+
2072
2156
  class Meta:
2073
2157
  verbose_name = "Formulário Dinâmico"
2074
2158
  verbose_name_plural = "Formulários Dinâmicos"
@@ -7622,6 +7622,13 @@ class SecaoApresentacaoCardsBlock(blocks.StructBlock):
7622
7622
  )
7623
7623
 
7624
7624
  # Título
7625
+ cor_fundo = blocks.ChoiceBlock(
7626
+ choices=BACKGROUND_COLOR_CHOICES,
7627
+ required=False,
7628
+ default='#6A1B9A', # Roxo como na imagem
7629
+ help_text="Cor de fundo do componente"
7630
+ )
7631
+
7625
7632
  titulo = blocks.CharBlock(
7626
7633
  required=True,
7627
7634
  max_length=200,
@@ -0,0 +1,62 @@
1
+ import re
2
+ from django.core.exceptions import ValidationError
3
+ from django.utils.translation import gettext_lazy as _
4
+
5
+ def validate_safe_characters(value):
6
+ """
7
+ Permite apenas letras, números, espaços e acentos
8
+ Bloqueia caracteres especiais perigosos E comandos SQL
9
+ """
10
+ if not isinstance(value, str) or not value:
11
+ return
12
+
13
+ # 1. Verificar caracteres permitidos primeiro
14
+ allowed_pattern = r'^[a-zA-Z0-9À-ÿ\s\.\-@]+$'
15
+
16
+ if not re.match(allowed_pattern, value):
17
+ raise ValidationError(
18
+ _('Este campo só aceita letras, números, espaços e acentos.'),
19
+ code='invalid_characters'
20
+ )
21
+
22
+ # 2. Verificar comandos SQL proibidos
23
+ sql_commands = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE']
24
+
25
+ # Converter para maiúsculo para comparação
26
+ value_upper = value.upper()
27
+
28
+ for command in sql_commands:
29
+ # Verificar se o comando aparece como palavra completa
30
+ if re.search(r'\b' + command + r'\b', value_upper):
31
+ raise ValidationError(
32
+ _('Este campo contém comandos não permitidos.'),
33
+ code='sql_command_detected'
34
+ )
35
+
36
+
37
+ def validate_email_field(value):
38
+ """
39
+ Para campos de email - permite @ e . mas também bloqueia comandos SQL
40
+ """
41
+ if not isinstance(value, str) or not value:
42
+ return
43
+
44
+ # 1. Verificar caracteres permitidos para email
45
+ allowed_pattern = r'^[a-zA-Z0-9@\.\-_]+$'
46
+
47
+ if not re.match(allowed_pattern, value):
48
+ raise ValidationError(
49
+ _('Email contém caracteres não permitidos.'),
50
+ code='invalid_email_characters'
51
+ )
52
+
53
+ # 2. Verificar comandos SQL também em emails
54
+ sql_commands = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE']
55
+ value_upper = value.upper()
56
+
57
+ for command in sql_commands:
58
+ if re.search(r'\b' + command + r'\b', value_upper):
59
+ raise ValidationError(
60
+ _('Email contém comandos não permitidos.'),
61
+ code='sql_command_in_email'
62
+ )
@@ -0,0 +1,116 @@
1
+ # enap_designsystem/blocks/middleware.py
2
+
3
+ from django.http import HttpResponseBadRequest
4
+ from django.core.exceptions import ValidationError
5
+ from ..blocks.security import validate_safe_characters, validate_email_field
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class CharacterFilterMiddleware:
11
+ """
12
+ Middleware que filtra caracteres perigosos - COM EXCEÇÕES PARA ADMIN
13
+ """
14
+ def __init__(self, get_response):
15
+ self.get_response = get_response
16
+
17
+ def __call__(self, request):
18
+ # Verificar apenas requisições POST
19
+ if request.method == 'POST':
20
+
21
+ # EXCEÇÕES: Não validar estes campos/URLs
22
+ if self.should_skip_validation(request):
23
+ return self.get_response(request)
24
+
25
+ # Verificar cada campo enviado
26
+ for key, value in request.POST.items():
27
+ if isinstance(value, str) and value.strip():
28
+
29
+ # Pular campos administrativos/técnicos
30
+ if self.is_admin_field(key):
31
+ continue
32
+
33
+ try:
34
+ # Para campos de email, usar validação específica
35
+ if 'email' in key.lower():
36
+ validate_email_field(value)
37
+ else:
38
+ # Para outros campos, validação geral
39
+ validate_safe_characters(value)
40
+
41
+ except ValidationError as e:
42
+ # Log da tentativa suspeita
43
+ logger.warning(f"Caracteres suspeitos bloqueados no campo '{key}': {value[:50]}")
44
+
45
+ # Retornar erro
46
+ return HttpResponseBadRequest(
47
+ f"Campo '{key}' contém caracteres não permitidos por motivos de segurança."
48
+ )
49
+
50
+ # Continuar processamento normal
51
+ return self.get_response(request)
52
+
53
+ def should_skip_validation(self, request):
54
+ """
55
+ Determina se deve pular validação para esta requisição
56
+ """
57
+ # URLs que devem ser ignoradas
58
+ skip_urls = [
59
+ '/admin/', # Painel admin Django
60
+ '/cms/', # Painel admin Wagtail (se usar esta URL)
61
+ '/django-admin/', # Admin alternativo
62
+ ]
63
+
64
+ # Verificar se a URL atual deve ser ignorada
65
+ for skip_url in skip_urls:
66
+ if request.path.startswith(skip_url):
67
+ return True
68
+
69
+ return False
70
+
71
+ def is_admin_field(self, field_name):
72
+ """
73
+ Identifica campos administrativos que devem ser ignorados
74
+ """
75
+ # Prefixos de campos administrativos
76
+ admin_prefixes = [
77
+ 'form_steps-', # StreamField do formulário
78
+ 'csrfmiddlewaretoken', # Token CSRF
79
+ 'action-', # Ações do admin
80
+ 'select_', # Seletores do admin
81
+ '_selected_', # Campos selecionados
82
+ 'logo_section-', # Campos de logo
83
+ 'background_image_fundo-', # Campos de imagem
84
+ 'thank_you_image_section-', # Campos de agradecimento
85
+ ]
86
+
87
+ # Sufixos de campos administrativos
88
+ admin_suffixes = [
89
+ '-type', # Tipo do bloco
90
+ '-deleted', # Campo deletado
91
+ '-order', # Ordem do campo
92
+ '-id', # ID do bloco
93
+ ]
94
+
95
+ # Verificar prefixos
96
+ for prefix in admin_prefixes:
97
+ if field_name.startswith(prefix):
98
+ return True
99
+
100
+ # Verificar sufixos
101
+ for suffix in admin_suffixes:
102
+ if field_name.endswith(suffix):
103
+ return True
104
+
105
+ # Campos específicos do Wagtail
106
+ wagtail_fields = [
107
+ 'title', # Título da página
108
+ 'slug', # Slug da página
109
+ 'seo_title', # SEO
110
+ 'search_description', # Descrição
111
+ ]
112
+
113
+ if field_name in wagtail_fields:
114
+ return True
115
+
116
+ return False