diff --git a/CHANGES.md b/CHANGES.md index e7b78bbd7..efac508cd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,28 @@ +3.1.165-RC3 / 2026-09-01 +======================== + + * Corrige SQL injection em DispositivoSearchFragmentFormView (#3856) + * Remove limite de caracteres do campo Informação Geral da Casa (#3848) + * Oculta sessões plenárias não iniciadas da consulta pública (#3845) + * hot-fix: Adiciona MIME Media Types opcionais para CSV. + * hot-fix: Remove alerta errado + * Corrige ordem dos expedientes no PDF do Resumo da Sessão (#3844) + * hot-fix: imprime somente contéudo da ocorrência + * Fix duplicate normas/matérias in author reports and prevent unfiltered queries + * Fix ValueError in audiencia form and AttributeError on HEAD requests + * Fix panel clock source and prevent overlapping polling + * hot-fix: recover epoch from server always + * Corrige polling excessivo e bloqueia não-votantes em /voto-individual/ (#3831) + * Hot-fix: corrige cópia de request.data + * Release: 3.1.165-RC2 + * #3821 centralizar geração próximo número para create de matérias legislativas (#3822) + +3.1.166-RC0 / 2026-09-01 +======================== + + + 3.1.165-RC2 / 2026-03-26 ======================== diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index dc8559812..46b5bd627 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -33,7 +33,7 @@ services: networks: - sapl-net sapl: - image: interlegis/sapl:3.1.165-RC2 + image: interlegis/sapl:3.1.165-RC3 # build: # context: ../ # dockerfile: ./docker/Dockerfile diff --git a/sapl/base/migrations/0061_casalegislativa_informacao_geral.py b/sapl/base/migrations/0061_casalegislativa_informacao_geral.py new file mode 100644 index 000000000..5fec68acd --- /dev/null +++ b/sapl/base/migrations/0061_casalegislativa_informacao_geral.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.28 on 2026-08-05 12:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0060_auto_20240812_1628'), + ] + + operations = [ + migrations.AlterField( + model_name='casalegislativa', + name='informacao_geral', + field=models.TextField(blank=True, + verbose_name='Informação Geral'), + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index 692c55138..b31c532ab 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -73,8 +73,10 @@ class CasaLegislativa(models.Model): max_length=100, blank=True, verbose_name=_('HomePage')) email = models.EmailField( max_length=100, blank=True, verbose_name=_('E-mail')) + # Campo editado via editor de texto rico (TinyMCE), portanto o valor + # armazenado é HTML. Não deve ter max_length: as tags consomem o limite + # e impedem o salvamento de textos curtos. informacao_geral = models.TextField( - max_length=100, blank=True, verbose_name=_('Informação Geral')) diff --git a/sapl/base/tests/test_form.py b/sapl/base/tests/test_form.py index 35f1c82bf..9464fe39e 100644 --- a/sapl/base/tests/test_form.py +++ b/sapl/base/tests/test_form.py @@ -39,3 +39,30 @@ def test_casa_legislativa_form_invalido(): }) assert not form.is_valid() + + +@pytest.mark.django_db(transaction=False) +def test_casa_legislativa_form_informacao_geral_sem_limite(): + # O campo é editado por editor de texto rico, então o valor submetido é + # HTML: as tags não podem consumir um limite de caracteres. + html = ''.join('
%s
' % linha for linha in + ['Câmara Municipal', 'Rua Osvaldo Cruz, 555 - Centro', + 'Expediente das 8h às 12h e das 13h30 às 17h30']) + assert len(html) > 100 + + form = CasaLegislativaForm(data={'codigo': 'codigo', + 'nome': 'nome', + 'sigla': 'sg', + 'endereco': 'endereco', + 'cep': '70000-000', + 'municipio': 'municipio', + 'uf': 'DF', + 'telefone': '33333333', + 'fax': '33333333', + 'endereco_web': '', + 'email': '', + 'informacao_geral': html + }) + + assert form.is_valid(), form.errors + assert form.cleaned_data['informacao_geral'] == html diff --git a/sapl/compilacao/utils.py b/sapl/compilacao/utils.py index aa1793484..c56f618b8 100644 --- a/sapl/compilacao/utils.py +++ b/sapl/compilacao/utils.py @@ -1,5 +1,7 @@ import sys +from django.utils.translation import ugettext_lazy as _ + DISPOSITIVO_SELECT_RELATED = ( 'tipo_dispositivo', 'ta_publicado', @@ -66,3 +68,14 @@ def get_integrations_view_names(): if 'IntegracaoTaView' in str(base): result.append(v) return result + + +def valid_int(value): + if value: + try: + int(value) + except (TypeError, ValueError): + raise ValueError( + _('Valor "%(value)s" não é um número inteiro válido.') % { + 'value': value}) + return value diff --git a/sapl/compilacao/views.py b/sapl/compilacao/views.py index 9afbe3201..93a066b3f 100644 --- a/sapl/compilacao/views.py +++ b/sapl/compilacao/views.py @@ -49,7 +49,7 @@ from sapl.compilacao.models import (STATUS_TA_EDITION, STATUS_TA_PRIVATE, VeiculoPublicacao, Vide) from sapl.compilacao.utils import (DISPOSITIVO_SELECT_RELATED, DISPOSITIVO_SELECT_RELATED_EDIT, - get_integrations_view_names) + get_integrations_view_names, valid_int) from sapl.crud.base import RP_DETAIL, RP_LIST, Crud, CrudAux, CrudListView,\ make_pagination from sapl.settings import BASE_DIR @@ -3177,14 +3177,14 @@ class DispositivoSearchFragmentFormView(ListView): result = [] try: - tipo_model = self.request.GET.get('tipo_model', '') + tipo_model = valid_int(self.request.GET.get('tipo_model', '')) limit = int(self.request.GET.get('max_results', 100)) - tipo_ta = self.request.GET.get('tipo_ta', '') + tipo_ta = valid_int(self.request.GET.get('tipo_ta', '')) num_ta = self.request.GET.get('num_ta', '') - ano_ta = self.request.GET.get('ano_ta', '') + ano_ta = valid_int(self.request.GET.get('ano_ta', '')) rotulo = self.request.GET.get('rotulo', '') str_texto = self.request.GET.get('texto', '') - texto = str_texto.split(' ') + texto_valores = str_texto.split(' ') tipo_resultado = self.request.GET.get('tipo_resultado', '') tipo_resultado = '' if tipo_resultado == 'False' else tipo_resultado @@ -3228,20 +3228,24 @@ class DispositivoSearchFragmentFormView(ListView): AND_CONTROLS = '''AND td.dispositivo_de_alteracao = true AND td.dispositivo_de_articulacao = true''' - texto = list(map("d.texto ~* '{}'".format, texto)) + texto = ['d.texto ~* %s'] * len(texto_valores) AND_TEXTO_ROTULO = '' + texto_rotulo_params = [] if str_texto and rotulo: - AND_TEXTO_ROTULO = '''AND ( ({BUSCA_TEXTO} AND d.rotulo ~* '{BUSCA_ROTULO}') OR - ({BUSCA_TEXTO} AND d.rotulo = '' AND dp.rotulo ~* '{BUSCA_ROTULO}') + busca_texto = ' AND '.join(texto) + AND_TEXTO_ROTULO = '''AND ( ({BUSCA_TEXTO} AND d.rotulo ~* %s) OR + ({BUSCA_TEXTO} AND d.rotulo = '' AND dp.rotulo ~* %s) )'''.format( - BUSCA_TEXTO=' AND '.join(texto), - BUSCA_ROTULO=rotulo + BUSCA_TEXTO=busca_texto, ) + texto_rotulo_params = ( + texto_valores + [rotulo] + texto_valores + [rotulo]) elif str_texto: - AND_TEXTO_ROTULO = ' AND %s' % ' AND '.join(texto) + AND_TEXTO_ROTULO = ' AND ' + ' AND '.join(texto) + texto_rotulo_params = texto_valores elif rotulo: - AND_TEXTO_ROTULO = "AND d.rotulo ~* '{BUSCA_ROTULO}'".format( - BUSCA_ROTULO=rotulo) + AND_TEXTO_ROTULO = "AND d.rotulo ~* %s" + texto_rotulo_params = [rotulo] else: AND_TEXTO_ROTULO = '' @@ -3288,17 +3292,21 @@ class DispositivoSearchFragmentFormView(ListView): AND3_TIPO_TA="AND ta.tipo_ta_id = {}".format( tipo_ta.id) if tipo_ta else '', - AND2_ANO="AND ta.ano = {}".format( - ano_ta) if ano_ta else '', + AND2_ANO="AND ta.ano = %s" if ano_ta else '', - AND1_NUMERO="AND ta.numero ~* '{}'".format( - num_ta) if num_ta else '', + AND1_NUMERO="AND ta.numero ~* %s" if num_ta else '', AND_TEXTO_ROTULO=AND_TEXTO_ROTULO if AND_TEXTO_ROTULO else '', AND_CONTROLS=AND_CONTROLS if AND_CONTROLS else '' ) - result = Dispositivo.objects.raw(sql) + params = texto_rotulo_params.copy() + if num_ta: + params.append(num_ta) + if ano_ta: + params.append(ano_ta) + + result = Dispositivo.objects.raw(sql, params) r = [] ids = set() diff --git a/sapl/settings.py b/sapl/settings.py index 4be58ab0c..9c9aa99c7 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -43,7 +43,7 @@ ALLOWED_HOSTS = ['*'] LOGIN_REDIRECT_URL = '/' LOGIN_URL = '/login/?next=' -SAPL_VERSION = '3.1.165-RC2' +SAPL_VERSION = '3.1.165-RC3' if DEBUG: EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/sapl/templates/base.html b/sapl/templates/base.html index 79b285a3a..a11841704 100644 --- a/sapl/templates/base.html +++ b/sapl/templates/base.html @@ -200,7 +200,7 @@ Desenvolvido pelo Interlegis em software livre e aberto. - Release: 3.1.165-RC2 + Release: 3.1.165-RC3