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.
- enap_designsystem/blocks/form.py +88 -4
- enap_designsystem/blocks/html_blocks.py +7 -0
- enap_designsystem/blocks/security.py +62 -0
- enap_designsystem/middleware/filtro_inputs.py +116 -0
- enap_designsystem/migrations/0412_alter_areaaluno_body_alter_concursoinovacao_banner_and_more.py +67580 -0
- enap_designsystem/static/enap_designsystem/blocks/semana.css +2 -0
- enap_designsystem/templates/enap_designsystem/blocks/cards_titles.html +1 -1
- enap_designsystem/templates/enap_designsystem/form_templates/formulario_page.html +152 -0
- enap_designsystem/templates/enap_designsystem/pages/page_search.html +13 -13
- {wagtail_enap_designsystem-1.2.1.138.dist-info → wagtail_enap_designsystem-1.2.1.140.dist-info}/METADATA +1 -1
- {wagtail_enap_designsystem-1.2.1.138.dist-info → wagtail_enap_designsystem-1.2.1.140.dist-info}/RECORD +14 -11
- {wagtail_enap_designsystem-1.2.1.138.dist-info → wagtail_enap_designsystem-1.2.1.140.dist-info}/WHEEL +0 -0
- {wagtail_enap_designsystem-1.2.1.138.dist-info → wagtail_enap_designsystem-1.2.1.140.dist-info}/licenses/LICENSE +0 -0
- {wagtail_enap_designsystem-1.2.1.138.dist-info → wagtail_enap_designsystem-1.2.1.140.dist-info}/top_level.txt +0 -0
enap_designsystem/blocks/form.py
CHANGED
|
@@ -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
|
-
|
|
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
|