From 333bcf743de2a851cdd8798ec5a4ce1bd34f76cb Mon Sep 17 00:00:00 2001 From: Edward Oliveira Date: Sat, 18 Apr 2026 16:33:17 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20serve=5Fimage=20view=20and=20semantic?= =?UTF-8?q?=20image=20URLs=20for=20all=20image=20fields=20(RFC=20=C2=A712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IMAGE_FIELDS allowlist and serve_image view: cover CasaLegislativa.logotipo, Partido.logo_partido, Parlamentar.fotografia, Dispositivo.imagem. The view validates the (app, model, field) triple, fetches the instance, and issues X-Accel-Redirect to nginx — same mechanism as serve_file but without FileMetadata involvement (images carry no versioning or access-control requirement). - nginx: /media/CACHE/ added as a public exception before the internal /media/ block so sorl-thumbnail cached thumbnails (fotografia via {% cropped_thumbnail %}) remain accessible to browsers without going through Django. - get_logotipo_url helper in sapl/utils.py: returns the semantic /imagens/base/casalegislativa//logotipo/ URL; avoids circular imports since sapl/base/views.py imports from sapl/relatorios/views.py. - LogotipoView updated to redirect to the semantic URL instead of the raw /media/ path. - parliament_info context processor: adds logotipo_url to every template context so base.html and 404.html can render the logo without MEDIA_URL concatenation. - 5 HTML templates updated: {% if logotipo %}{{ MEDIA_URL }}{{ logotipo }} replaced with {% if logotipo_url %}{{ logotipo_url }}. - relatorios/views.py (4 sites): logotipo_url added to header_context dicts passed to header_ata.html. - painel/views.py: brasao computed via get_logotipo_url instead of casa.logotipo.url (which returns the now-internal /media/ path). - ImageThumbnailFileInput.get_context: computes semantic_url from IMAGE_FIELDS when the instance has a PK; image_thumbnail.html uses it as the src fallback so the edit-form preview remains visible after /media/ became internal. Co-Authored-By: Claude Sonnet 4.6 --- docker/config/nginx/sapl.conf | 4 ++ sapl/base/views.py | 47 ++++++++++++++++++- sapl/context_processors.py | 8 ++-- sapl/painel/views.py | 4 +- sapl/relatorios/views.py | 12 +++-- sapl/templates/404.html | 2 +- sapl/templates/base.html | 2 +- sapl/templates/materia/recibo_proposicao.html | 2 +- sapl/templates/protocoloadm/comprovante.html | 2 +- sapl/templates/relatorios/header_ata.html | 2 +- sapl/templates/widgets/image_thumbnail.html | 4 +- sapl/urls.py | 6 ++- sapl/utils.py | 37 +++++++++++++++ 13 files changed, 112 insertions(+), 20 deletions(-) diff --git a/docker/config/nginx/sapl.conf b/docker/config/nginx/sapl.conf index 91b73a94a..12bb24aff 100644 --- a/docker/config/nginx/sapl.conf +++ b/docker/config/nginx/sapl.conf @@ -49,6 +49,10 @@ server { alias /var/interlegis/sapl/collected_static/; } + location /media/CACHE/ { + alias /var/interlegis/sapl/media/CACHE/; + } + location /media/ { internal; alias /var/interlegis/sapl/media/; diff --git a/sapl/base/views.py b/sapl/base/views.py index d9e159fa0..ab9024515 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -1513,8 +1513,8 @@ class LogotipoView(RedirectView): def get_redirect_url(self, *args, **kwargs): casa = get_casalegislativa() - logo = casa and casa.logotipo and casa.logotipo.name - return os.path.join(settings.MEDIA_URL, logo) if logo else STATIC_LOGO + url = get_logotipo_url(casa) + return url if url else STATIC_LOGO def filtro_campos(dicionario): @@ -1664,3 +1664,46 @@ def serve_model_file(request, app_label, model_name, pk, field_name): raise Http404 return serve_file(request, file_uuid=meta.uuid) + + +# Image fields served via X-Accel-Redirect — same nginx internal mechanism as +# serve_file but without FileMetadata involvement (images carry no versioning or +# access-control requirement). An explicit allowlist prevents arbitrary ORM +# traversal (RFC §12.3). +IMAGE_FIELDS = frozenset([ + ('base', 'casalegislativa', 'logotipo'), + ('parlamentares', 'partido', 'logo_partido'), + ('parlamentares', 'parlamentar', 'fotografia'), + ('compilacao', 'dispositivo', 'imagem'), +]) + + +def serve_image(request, app_label, model_name, pk, field_name): + """ + Serve an image field via nginx X-Accel-Redirect (RFC §12.4). + + All four image field models are unconditionally public — no permission + check is performed. The allowlist is the only gate. + """ + from django.shortcuts import get_object_or_404 as _get_or_404 + + if (app_label, model_name, field_name) not in IMAGE_FIELDS: + raise Http404 + try: + model = apps.get_model(app_label, model_name) + except LookupError: + raise Http404 + + instance = _get_or_404(model, pk=pk) + field_file = getattr(instance, field_name, None) + if not field_file: + raise Http404 + + response = HttpResponse() + response['X-Accel-Redirect'] = f'/media/{field_file.name}' + return response + + +def get_logotipo_url(casa): + from sapl.utils import get_logotipo_url as _get_logotipo_url + return _get_logotipo_url(casa) diff --git a/sapl/context_processors.py b/sapl/context_processors.py index bded6d7e7..4c903b11e 100644 --- a/sapl/context_processors.py +++ b/sapl/context_processors.py @@ -9,11 +9,13 @@ from sapl.utils import mail_service_configured as mail_service_configured_utils def parliament_info(request): from sapl.base.views import get_casalegislativa + from sapl.utils import get_logotipo_url casa = get_casalegislativa() if casa: - return casa.__dict__ - else: - return {} + ctx = dict(casa.__dict__) + ctx['logotipo_url'] = get_logotipo_url(casa) + return ctx + return {} def mail_service_configured(request): diff --git a/sapl/painel/views.py b/sapl/painel/views.py index cea4e2870..ff95341cc 100644 --- a/sapl/painel/views.py +++ b/sapl/painel/views.py @@ -22,7 +22,7 @@ from sapl.sessao.models import (ExpedienteMateria, OradorExpediente, OrdemDia, PresencaOrdemDia, RegistroVotacao, SessaoPlenaria, SessaoPlenariaPresenca, VotoParlamentar, RegistroLeitura) -from sapl.utils import filiacao_data, get_client_ip, sort_lista_chave +from sapl.utils import filiacao_data, get_client_ip, sort_lista_chave, get_logotipo_url from .models import Cronometro @@ -555,7 +555,7 @@ def get_dados_painel(request, pk): brasao = None if casa and app_config and (bool(casa.logotipo)): - brasao = casa.logotipo.url \ + brasao = get_logotipo_url(casa) \ if app_config.mostrar_brasao_painel else None response = { diff --git a/sapl/relatorios/views.py b/sapl/relatorios/views.py index bc28b3ffc..aa7b0743f 100755 --- a/sapl/relatorios/views.py +++ b/sapl/relatorios/views.py @@ -51,7 +51,7 @@ from sapl.sessao.views import (get_identificacao_basica, get_mesa_diretora, from sapl.settings import MEDIA_URL, RATE_LIMITER_RATE from sapl.settings import STATIC_ROOT from sapl.utils import LISTA_DE_UFS, TrocaTag, filiacao_data, create_barcode, show_results_filter_set, \ - num_materias_por_tipo, parlamentares_ativos, MultiFormatOutputMixin, ratelimit_ip + num_materias_por_tipo, parlamentares_ativos, MultiFormatOutputMixin, ratelimit_ip, get_logotipo_url from .templates import (pdf_capa_processo_gerar, pdf_documento_administrativo_gerar, pdf_espelho_gerar, pdf_etiqueta_protocolo_gerar, pdf_materia_gerar, @@ -1460,8 +1460,8 @@ def resumo_ata_pdf(request, pk): 'decimo_quinto_ordenacao': 'ocorrencias_da_sessao.html', 'decimo_sexto_ordenacao': 'consideracoes_finais.html' }) - header_context = {"casa": casa, - 'logotipo': casa.logotipo, 'MEDIA_URL': MEDIA_URL} + header_context = {"casa": casa, 'logotipo': casa.logotipo, + 'logotipo_url': get_logotipo_url(casa), 'MEDIA_URL': MEDIA_URL} html_template = render_to_string('relatorios/relatorio_ata.html', context) html_header = render_to_string( @@ -1487,6 +1487,7 @@ def cria_relatorio(request, context, html_string, header_info=""): context.update({'rodape': rodape}) header_context = {"casa": casa, 'logotipo': casa.logotipo, + 'logotipo_url': get_logotipo_url(casa), 'MEDIA_URL': MEDIA_URL, 'info': header_info} html_template = render_to_string(html_string, context) @@ -1713,6 +1714,7 @@ def relatorio_sessao_plenaria_pdf(request, pk): html_header = render_to_string('relatorios/header_ata.html', {"casa": casa, "MEDIA_URL": MEDIA_URL, "logotipo": casa.logotipo, + "logotipo_url": get_logotipo_url(casa), "info": info}) pdf_file = make_pdf( @@ -1795,8 +1797,8 @@ def relatorio_materia_tramitacao(request, pk): 'rodape': rodape, 'data': dt.today().strftime('%d/%m/%Y'), 'rodape': rodape}) - header_context = {"casa": casa, - 'logotipo': casa.logotipo, 'MEDIA_URL': MEDIA_URL} + header_context = {"casa": casa, 'logotipo': casa.logotipo, + 'logotipo_url': get_logotipo_url(casa), 'MEDIA_URL': MEDIA_URL} html_template = render_to_string( 'relatorios/relatorio_materia_tramitacao.html', context) diff --git a/sapl/templates/404.html b/sapl/templates/404.html index 82a3dee5e..f2bc5c501 100644 --- a/sapl/templates/404.html +++ b/sapl/templates/404.html @@ -49,7 +49,7 @@