Browse Source

Corrige escape duplo e cobertura da sanitização de TextField

Ajustes da revisão do #3846:

- plain: grava texto puro (entidades decodificadas); o escape fica na
  renderização. Antes, & e < apareciam como &amp; e &lt; na tela e na API.
- get_field_display: escapa campos plain e isentos em vez de re-sanitizar.
- Blocos da ata: |sanitize|striptags, sem escapar &nbsp;/&amp; duas vezes.
- RICH_TEXT_FIELDS: inclui os campos editados no TinyMCE que faltavam
  (biografia, informacao_geral, NormaRelacionada.resumo,
  Legislatura.observacao); perfil público renderiza biografia com |sanitize.
- compilacao: Dispositivo.texto passa de |safe para |sanitize nos cinco
  templates restantes. TipoDispositivo.*_html continua na política rich no
  pre_save; o form de edição rejeita fragmentos com tags desbalanceadas,
  que a sanitização descartaria ao salvar.
- pre_save ignora raw=True (loaddata, inclusive em migrations).
- Isenção por campo (SANITIZE_EXEMPT_FIELDS), como RICH_TEXT_FIELDS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hotfix/sanitize-textfield-xss
Edward Ribeiro 1 week ago
parent
commit
ad04976432
  1. 3
      sapl/base/receivers.py
  2. 83
      sapl/base/tests/test_sanitize.py
  3. 28
      sapl/compilacao/forms.py
  4. 4
      sapl/compilacao/views.py
  5. 11
      sapl/crispy_layout_mixin.py
  6. 76
      sapl/sanitize.py
  7. 4
      sapl/templates/compilacao/dispositivo_form_search_fragment_child.html
  8. 2
      sapl/templates/compilacao/layout/dispositivo_checkbox.html
  9. 2
      sapl/templates/compilacao/layout/dispositivo_radio.html
  10. 2
      sapl/templates/compilacao/text_edit_blocoalteracao.html
  11. 2
      sapl/templates/compilacao/text_notificacoes.html
  12. 2
      sapl/templates/parlamentares/parlamentar_perfil_publico.html
  13. 3
      sapl/templates/relatorios/blocos_sessao_plenaria/consideracoes_finais.html
  14. 3
      sapl/templates/relatorios/blocos_sessao_plenaria/ocorrencias_da_sessao.html
  15. 3
      sapl/templates/sessao/blocos_ata/consideracoes_finais.html
  16. 3
      sapl/templates/sessao/blocos_ata/expedientes.html
  17. 3
      sapl/templates/sessao/blocos_ata/ocorrencias_da_sessao.html

3
sapl/base/receivers.py

@ -504,6 +504,9 @@ def sanitize_textfields_pre_save_signal(sender, instance, **kwargs):
Cobre forms, a API do drfautoapi, o admin e o shell num único ponto.
Ver sapl.sanitize para as políticas por campo.
"""
# loaddata (inclusive dentro de migrations) grava o objeto literalmente
if kwargs.get('raw'):
return
if sender._meta.app_label not in SAPL_APP_LABELS:
return

83
sapl/base/tests/test_sanitize.py

@ -1,10 +1,16 @@
import pytest
from django.db.models.signals import pre_save
from django.template import Context, Template
from model_bakery import baker
from sapl.compilacao.forms import TipoDispositivoForm
from sapl.compilacao.models import TipoDispositivo, TipoTextoArticulado
from sapl.crispy_layout_mixin import get_field_display
from sapl.lexml.models import LexmlProvedor
from sapl.parlamentares.models import Parlamentar
from sapl.protocoloadm.models import TramitacaoAdministrativo
from sapl.sanitize import sanitize_field, sanitize_html, sanitize_scope
from sapl.sanitize import (html_fragment_is_balanced, sanitize_field,
sanitize_html, sanitize_scope)
from sapl.sessao.models import ExpedienteSessao
@ -16,9 +22,10 @@ def test_plain_remove_marcacao_e_preserva_texto():
assert sanitize_html('<div>a</div><div>b</div>') == 'ab'
def test_plain_escapa_caracteres_especiais():
assert sanitize_html('Valor < 10 & prazo > 5') == \
'Valor &lt; 10 &amp; prazo &gt; 5'
def test_plain_guarda_texto_puro_sem_escape():
"""O escape é da renderização; gravado escapado, apareceria &amp; na tela."""
assert sanitize_html('Valor < 10 & prazo > 5') == 'Valor < 10 & prazo > 5'
assert sanitize_html('ALFA &amp; BETA') == 'ALFA & BETA'
def test_plain_preserva_quebras_de_linha():
@ -39,11 +46,7 @@ def test_valores_vazios_atravessam():
'texto &amp; cia',
])
def test_plain_e_idempotente(valor):
"""Propriedade da qual dependem as duas camadas (pre_save + renderização).
Se sanitizar duas vezes não fosse estável, o valor gravado seria
re-escapado a cada exibição.
"""
"""Salvar de novo um registro já sanitizado não altera o texto."""
uma_vez = sanitize_html(valor)
assert sanitize_html(uma_vez) == uma_vez
@ -108,6 +111,8 @@ def test_sanitize_scope():
assert sanitize_scope(TramitacaoAdministrativo, 'texto') == 'plain'
assert sanitize_scope(ExpedienteSessao, 'conteudo') == 'rich'
assert sanitize_scope(LexmlProvedor, 'xml') == 'exempt'
assert sanitize_scope(TipoTextoArticulado, 'rodape_global') == 'exempt'
assert sanitize_scope(Parlamentar, 'biografia') == 'rich'
def test_sanitize_field_respeita_isencao():
@ -161,3 +166,63 @@ def test_get_field_display_protege_linha_legada():
__, display = get_field_display(t, 'texto')
assert '<script>' not in display
def test_pre_save_ignora_raw():
"""loaddata (inclusive em migrations) grava o objeto literalmente."""
t = TramitacaoAdministrativo(texto='<b>fixture</b>')
pre_save.send(sender=TramitacaoAdministrativo, instance=t, raw=True)
assert t.texto == '<b>fixture</b>'
def test_get_field_display_escapa_texto_puro_uma_unica_vez():
t = TramitacaoAdministrativo(texto='ALFA & BETA < 30')
__, display = get_field_display(t, 'texto')
assert 'ALFA &amp; BETA &lt; 30' in display
assert '&amp;amp;' not in display
def test_get_field_display_escapa_campo_isento():
p = LexmlProvedor(xml='<xml><script>x</script></xml>')
__, display = get_field_display(p, 'xml')
assert '<script>' not in display
assert '&lt;xml&gt;' in display
def test_striptags_apos_sanitize_nao_escapa_duas_vezes():
"""Blocos da ata: texto puro, sem entidades escapadas de novo."""
t = Template('{% load common_tags %}{{ v|sanitize|striptags }}')
saida = t.render(Context({
'v': '<p>Ofício&nbsp;12 lido &amp; arquivado</p>'
'<script>alert(1)</script><img src=x onerror=alert(1)>'}))
assert saida == 'Ofício&nbsp;12 lido &amp; arquivado'
@pytest.mark.parametrize('valor, esperado', [
('<br>', True),
('<br/>', True),
('<div class="titulo">Justificativa</div>', True),
('Art. ', True),
('<span class="x">', False),
('</span>', False),
('<b><i>x</b></i>', False),
])
def test_html_fragment_is_balanced(valor, esperado):
assert html_fragment_is_balanced(valor) is esperado
@pytest.mark.django_db
def test_tipo_dispositivo_form_rejeita_fragmento_desbalanceado():
td = baker.make(TipoDispositivo)
dados = {f: getattr(td, f) or '' for f in TipoDispositivoForm.Meta.fields}
dados['rotulo_prefixo_html'] = '<span class="rotulo">'
dados['rotulo_sufixo_html'] = '</span>'
form = TipoDispositivoForm(data=dados, instance=td)
assert not form.is_valid()
assert 'rotulo_prefixo_html' in form.errors
assert 'rotulo_sufixo_html' in form.errors
dados['rotulo_prefixo_html'] = '<br/>'
dados['rotulo_sufixo_html'] = ''
form = TipoDispositivoForm(data=dados, instance=td)
assert 'rotulo_prefixo_html' not in form.errors

28
sapl/compilacao/forms.py

@ -26,6 +26,7 @@ from sapl.compilacao.models import (NOTAS_PUBLICIDADE_CHOICES,
from sapl.compilacao.utils import DISPOSITIVO_SELECT_RELATED
from sapl.crispy_layout_mixin import SaplFormHelper
from sapl.crispy_layout_mixin import SaplFormLayout, to_column, to_row
from sapl.sanitize import RICH_TEXT_FIELDS, html_fragment_is_balanced
from sapl.utils import YES_NO_CHOICES, FileFieldCheckMixin
@ -51,6 +52,33 @@ ta_error_messages = {
}
class TipoDispositivoForm(ModelForm):
class Meta:
model = TipoDispositivo
fields = ['rotulo_ordinal', 'formato_variacao0',
'rotulo_separador_variacao01', 'formato_variacao1',
'rotulo_separador_variacao12', 'formato_variacao2',
'rotulo_separador_variacao23', 'formato_variacao3',
'rotulo_separador_variacao34', 'formato_variacao4',
'rotulo_separador_variacao45', 'formato_variacao5',
'rotulo_prefixo_html', 'rotulo_sufixo_html',
'texto_prefixo_html', 'texto_sufixo_html',
'nota_automatica_prefixo_html',
'nota_automatica_sufixo_html']
def clean(self):
cleaned_data = super().clean()
for fieldname in RICH_TEXT_FIELDS['compilacao.TipoDispositivo']:
value = cleaned_data.get(fieldname)
if value and not html_fragment_is_balanced(value):
self.add_error(fieldname, _(
'Toda tag aberta neste campo deve ser fechada nele '
'mesmo; tags divididas entre prefixo e sufixo são '
'descartadas ao salvar.'))
return cleaned_data
class TipoTaForm(ModelForm):
sigla = forms.CharField(
label=TipoTextoArticulado._meta.get_field(

4
sapl/compilacao/views.py

@ -39,7 +39,8 @@ from sapl.compilacao.forms import (DispositivoDefinidorVigenciaForm,
DispositivoRegistroRevogacaoForm,
DispositivoSearchModalForm, NotaForm,
PublicacaoForm, TaForm,
TextNotificacoesForm, TipoTaForm, VideForm)
TextNotificacoesForm, TipoDispositivoForm,
TipoTaForm, VideForm)
from sapl.compilacao.models import (STATUS_TA_EDITION, STATUS_TA_PRIVATE,
STATUS_TA_PUBLIC, Dispositivo, Nota,
PerfilEstruturalTextoArticulado,
@ -85,6 +86,7 @@ class TipoDispositivoCrud(CrudAux):
class UpdateView(CrudAux.UpdateView):
layout_key = 'TipoDispositivoUpdate'
form_class = TipoDispositivoForm
class ListView(CrudAux.ListView):
paginate_by = 100

11
sapl/crispy_layout_mixin.py

@ -8,10 +8,11 @@ from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, reverse_lazy
from django.utils import formats
from django.utils.encoding import force_text
from django.utils.html import escape
from django.utils.translation import ugettext as _
import yaml
from sapl.sanitize import sanitize_field
from sapl.sanitize import sanitize_html, sanitize_scope
def heads_and_tails(list_of_lists):
@ -169,7 +170,13 @@ def get_field_display(obj, fieldname):
args=(value.id,)),
value)
elif 'TextField' in str_type_from_field:
display = sanitize_field(obj._meta.model, fieldname, value)
scope = sanitize_scope(obj._meta.model, fieldname)
if scope == 'rich':
display = sanitize_html(value, rich=True)
elif scope == 'plain':
display = escape(sanitize_html(value))
else:
display = escape(value)
display = display.replace('\n', '<br/>')
display = '<div class="dont-break-out">{}</div>'.format(display)
else:

76
sapl/sanitize.py

@ -6,16 +6,22 @@ e ``sapl.utils`` já importa ``sapl.crispy_layout_mixin``.
Duas políticas:
* ``plain`` — remove toda a marcação e preserva o texto. É o padrão para
* ``plain`` — remove toda a marcação e guarda texto puro, com as entidades
já decodificadas: o escape fica por conta da renderização. É o padrão para
qualquer ``TextField``.
* ``rich`` — allowlist para os campos editados via TinyMCE, que contêm HTML
legítimo (negrito, listas, tabelas e links).
Ambas são idempotentes: aplicar duas vezes produz o mesmo resultado. É essa
propriedade que permite sanitizar tanto no ``pre_save`` quanto na renderização
sem que os efeitos se acumulem.
A ``rich`` é idempotente, o que permite aplicá-la tanto no ``pre_save`` quanto
na renderização. A ``plain`` só não é idempotente para texto que seja HTML
codificado em entidades (``&lt;b&gt;``): a primeira passada decodifica, a
segunda remove a tag. Na renderização, campos ``plain`` são escapados em vez
de re-sanitizados.
"""
import html
from html.parser import HTMLParser
import nh3
SANITIZE_RICH_TAGS = {
@ -54,6 +60,10 @@ SANITIZE_CLEAN_CONTENT_TAGS = {'script', 'style'}
# compilacao.TipoDispositivo são fragmentos de template configurados por
# administradores.
RICH_TEXT_FIELDS = {
'base.CasaLegislativa': {'informacao_geral'},
'norma.NormaRelacionada': {'resumo'},
'parlamentares.Legislatura': {'observacao'},
'parlamentares.Parlamentar': {'biografia'},
'sessao.ExpedienteSessao': {'conteudo'},
'sessao.OcorrenciaSessao': {'conteudo'},
'sessao.ConsideracoesFinais': {'conteudo'},
@ -65,12 +75,12 @@ RICH_TEXT_FIELDS = {
},
}
# Modelos cujos TextField não devem ser tocados em hipótese alguma.
SANITIZE_EXEMPT_MODELS = {
# Campos que não devem ser tocados em hipótese alguma.
SANITIZE_EXEMPT_FIELDS = {
# xml é XML fornecido pela equipe do LexML; já é escapado em pretty_xml
'lexml.LexmlProvedor',
'lexml.LexmlProvedor': {'xml'},
# rodape_global é interpolado dentro de um content: de CSS
'compilacao.TipoTextoArticulado',
'compilacao.TipoTextoArticulado': {'rodape_global'},
}
@ -81,7 +91,7 @@ def model_key(model):
def sanitize_scope(model, fieldname):
"""Retorna 'exempt', 'rich' ou 'plain' para um campo de um modelo."""
key = model_key(model)
if key in SANITIZE_EXEMPT_MODELS:
if fieldname in SANITIZE_EXEMPT_FIELDS.get(key, ()):
return 'exempt'
if fieldname in RICH_TEXT_FIELDS.get(key, ()):
return 'rich'
@ -91,7 +101,8 @@ def sanitize_scope(model, fieldname):
def sanitize_html(value, rich=False):
"""Remove HTML/JavaScript perigoso de ``value``.
Com ``rich=False`` toda a marcação é removida e apenas o texto sobra.
Com ``rich=False`` toda a marcação é removida e apenas o texto sobra,
sem escape: ``&`` continua ``&``.
Com ``rich=True`` aplica-se a allowlist: links são preservados, mas
esquemas de URL fora de SANITIZE_URL_SCHEMES (javascript:, data:) e
manipuladores de evento (onclick, onerror) são descartados.
@ -112,21 +123,60 @@ def sanitize_html(value, rich=False):
link_rel='noopener noreferrer',
strip_comments=True)
return nh3.clean(
return html.unescape(nh3.clean(
value,
tags=set(),
attributes={},
clean_content_tags=SANITIZE_CLEAN_CONTENT_TAGS,
link_rel=None,
strip_comments=True)
strip_comments=True))
def sanitize_field(model, fieldname, value):
"""Sanitiza ``value`` conforme a política do campo.
Campos de modelos isentos atravessam sem modificação.
Campos isentos atravessam sem modificação.
"""
scope = sanitize_scope(model, fieldname)
if scope == 'exempt':
return value
return sanitize_html(value, rich=(scope == 'rich'))
_VOID_TAGS = {
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link',
'meta', 'param', 'source', 'track', 'wbr',
}
class _BalanceChecker(HTMLParser):
def __init__(self):
super().__init__(convert_charrefs=True)
self.stack = []
self.balanced = True
def handle_starttag(self, tag, attrs):
if tag not in _VOID_TAGS:
self.stack.append(tag)
def handle_startendtag(self, tag, attrs):
pass
def handle_endtag(self, tag):
if tag in _VOID_TAGS:
return
if not self.stack or self.stack.pop() != tag:
self.balanced = False
def html_fragment_is_balanced(value):
"""Indica se toda tag aberta em ``value`` é fechada nele mesmo.
Fragmentos desbalanceados (``<span>`` num campo, ``</span>`` em outro)
são reescritos pela sanitização ``rich``, que fecha ou descarta as tags.
"""
checker = _BalanceChecker()
checker.feed(value)
checker.close()
return checker.balanced and not checker.stack

4
sapl/templates/compilacao/dispositivo_form_search_fragment_child.html

@ -1,4 +1,4 @@
{% load i18n compilacao_filters %}
{% load i18n compilacao_filters common_tags %}
<li>
<div class="iteminput">
@ -30,7 +30,7 @@
{{ df.tipo_dispositivo.rotulo_prefixo_html|safe }}
<a name="{{df.pk}}" title="{{df.pk}}">{% if df.rotulo or df.nivel == 1%}{{ df.rotulo }}{%else%}[{{ df|nomenclatura}} {% trans "de" %} {{ df.dispositivo_pai.rotulo }}] - {% endif %}</a>
{{ df.tipo_dispositivo.rotulo_sufixo_html|safe }}
<span class="dtxt" id="d{% if not df.dispositivo_subsequente_id and df.dispositivo_substituido_id %}a{% endif %}{{df.pk}}" pks="{{df.dispositivo_substituido_id|default:''}}" pk="{{df.pk}}">{{ df.tipo_dispositivo.texto_prefixo_html|safe }}{%if df.texto %}{{ df.texto|safe }}{%else%}{%if not df.tipo_dispositivo.dispositivo_de_articulacao %}&nbsp;{% endif %}{% endif %}</span>
<span class="dtxt" id="d{% if not df.dispositivo_subsequente_id and df.dispositivo_substituido_id %}a{% endif %}{{df.pk}}" pks="{{df.dispositivo_substituido_id|default:''}}" pk="{{df.pk}}">{{ df.tipo_dispositivo.texto_prefixo_html|safe }}{%if df.texto %}{{ df.texto|sanitize }}{%else%}{%if not df.tipo_dispositivo.dispositivo_de_articulacao %}&nbsp;{% endif %}{% endif %}</span>
{% if df.ta_publicado_id %}
<a class="nota-alteracao" href="{% url 'sapl.compilacao:ta_text' df.ta_publicado.pk %}#{{df.dispositivo_atualizador_id}}">
{{ df.tipo_dispositivo.nota_automatica_prefixo_html|safe }}

2
sapl/templates/compilacao/layout/dispositivo_checkbox.html

@ -37,7 +37,7 @@
{{ choice.1.tipo_dispositivo.rotulo_prefixo_html|safe }}
<a name="{{choice.1.pk}}" title="{{choice.1.pk}}">{% if choice.1.rotulo %}{{ choice.1.rotulo }}{%else%}[{{ choice.1|nomenclatura}}{% if choice.1.dispositivo_pai %} {% trans "de" %} {{ choice.1.dispositivo_pai.rotulo }}{% endif %}] - {% endif %}</a>
{{ choice.1.tipo_dispositivo.rotulo_sufixo_html|safe }}
<span class="dtxt" id="d{% if not choice.1.dispositivo_subsequente_id and choice.1.dispositivo_substituido_id %}a{% endif %}{{choice.1.pk}}" pks="{{choice.1.dispositivo_substituido_id|default:''}}" pk="{{choice.1.pk}}">{{ choice.1.tipo_dispositivo.texto_prefixo_html|safe }}{%if choice.1.texto %}{{ choice.1.texto|safe }}{%else%}{%if not choice.1.tipo_dispositivo.dispositivo_de_articulacao %}&nbsp;{% endif %}{% endif %}</span>
<span class="dtxt" id="d{% if not choice.1.dispositivo_subsequente_id and choice.1.dispositivo_substituido_id %}a{% endif %}{{choice.1.pk}}" pks="{{choice.1.dispositivo_substituido_id|default:''}}" pk="{{choice.1.pk}}">{{ choice.1.tipo_dispositivo.texto_prefixo_html|safe }}{%if choice.1.texto %}{{ choice.1.texto|sanitize }}{%else%}{%if not choice.1.tipo_dispositivo.dispositivo_de_articulacao %}&nbsp;{% endif %}{% endif %}</span>
{% if choice.1.ta_publicado_id and not choice.1.tipo_dispositivo.dispositivo_de_articulacao %}
<a class="nota-alteracao" target="_blank" href="{% url 'sapl.compilacao:ta_text' choice.1.ta_publicado.pk %}#{{choice.1.dispositivo_atualizador_id}}">
{{ choice.1.tipo_dispositivo.nota_automatica_prefixo_html|safe }}

2
sapl/templates/compilacao/layout/dispositivo_radio.html

@ -35,7 +35,7 @@
<span class="dtxt"
id="d{% if not dpt.dispositivo_subsequente_id and dpt.dispositivo_substituido_id %}a{% endif %}{{dpt.pk}}"
pks="{{dpt.dispositivo_substituido_id|default:''}}"
pk="{{dpt.pk}}">{{ dpt.tipo_dispositivo.texto_prefixo_html|safe }}{%if dpt.texto %}{{ dpt.texto|safe }}{%else%}{%if not dpt.tipo_dispositivo.dispositivo_de_articulacao %}&nbsp;{% endif %}{% endif %}</span>
pk="{{dpt.pk}}">{{ dpt.tipo_dispositivo.texto_prefixo_html|safe }}{%if dpt.texto %}{{ dpt.texto|sanitize }}{%else%}{%if not dpt.tipo_dispositivo.dispositivo_de_articulacao %}&nbsp;{% endif %}{% endif %}</span>
{% if dpt.ta_publicado_id and not dpt.tipo_dispositivo.dispositivo_de_articulacao %}
<a class="nota-alteracao" href="{%url 'sapl.compilacao:ta_text' dpt.ta_publicado.pk %}#{{dpt.dispositivo_atualizador_id}}">
{{ dpt.tipo_dispositivo.nota_automatica_prefixo_html|safe }}

2
sapl/templates/compilacao/text_edit_blocoalteracao.html

@ -5,7 +5,7 @@
<div class="dpt" id="dpt{{ch.id}}" pk="{{ch.id}}" >
<div class="{{ ch.tipo_dispositivo.class_css }}" id="id{{ch.id}}" nivel="{{ch.nivel}}" style="margin: 0px;">
<div class="de-compilacao" id="id{{ch.id}}" pk="{{ch.pk}}" ordem="{{ch.ordem}}" name="{{ch.pk}}" title="{{ch.pk}} - {{ch.ordem}}">
{{ ch.tipo_dispositivo.rotulo_prefixo_html|safe }}<a name="{{ch.pk}}" href="{% url 'sapl.compilacao:ta_text_edit' ch.ta.pk %}#{{ch.pk}}">{{ ch.rotulo }}</a>{{ ch.tipo_dispositivo.rotulo_sufixo_html|safe }}{{ ch.tipo_dispositivo.texto_prefixo_html|safe }}{{ ch.texto|safe }}
{{ ch.tipo_dispositivo.rotulo_prefixo_html|safe }}<a name="{{ch.pk}}" href="{% url 'sapl.compilacao:ta_text_edit' ch.ta.pk %}#{{ch.pk}}">{{ ch.rotulo }}</a>{{ ch.tipo_dispositivo.rotulo_sufixo_html|safe }}{{ ch.tipo_dispositivo.texto_prefixo_html|safe }}{{ ch.texto|sanitize }}
</div>
</div>
</div>

2
sapl/templates/compilacao/text_notificacoes.html

@ -23,7 +23,7 @@
<span class="dtxt"
id="d{% if not dpt.dispositivo_subsequente_id and dpt.dispositivo_substituido_id %}a{% endif %}{{dpt.pk}}"
pks="{{dpt.dispositivo_substituido_id|default:''}}"
pk="{{dpt.pk}}">{{ dpt.tipo_dispositivo.texto_prefixo_html|safe }}{%if dpt.texto %}{{ dpt.texto|safe }}{%else%}{%if not dpt.tipo_dispositivo.dispositivo_de_articulacao %}&nbsp;{% endif %}{% endif %}</span>
pk="{{dpt.pk}}">{{ dpt.tipo_dispositivo.texto_prefixo_html|safe }}{%if dpt.texto %}{{ dpt.texto|sanitize }}{%else%}{%if not dpt.tipo_dispositivo.dispositivo_de_articulacao %}&nbsp;{% endif %}{% endif %}</span>
{%if dpt.ta_publicado_id %}
<a class="nota-alteracao clearfix" href="{%url 'sapl.compilacao:ta_text' dpt.ta_publicado.pk %}#{{dpt.dispositivo_atualizador_id}}">

2
sapl/templates/parlamentares/parlamentar_perfil_publico.html

@ -61,7 +61,7 @@
<div class="col-sm-8">
<div id="div_biografia" class="form-group">
<p><b>Biografia: </b> &nbsp {{object.biografia|linebreaksbr}}</p>
<p><b>Biografia: </b> &nbsp {{object.biografia|sanitize}}</p>
</div>
</div>
</div>

3
sapl/templates/relatorios/blocos_sessao_plenaria/consideracoes_finais.html

@ -1,4 +1,5 @@
{% load common_tags %}
<h2 class="gray-title">Considerações Finais</h2>
{% for c in lst_consideracoes%}
<p>{{c|striptags}}</p>
<p>{{c|sanitize|striptags}}</p>
{% endfor %}

3
sapl/templates/relatorios/blocos_sessao_plenaria/ocorrencias_da_sessao.html

@ -1,4 +1,5 @@
{% load common_tags %}
<h2 class="gray-title">Ocorrências da Sessão</h2>
{% for o in lst_ocorrencias%}
<p>{{o.conteudo|striptags}}</p>
<p>{{o.conteudo|sanitize|striptags}}</p>
{% endfor %}

3
sapl/templates/sessao/blocos_ata/consideracoes_finais.html

@ -1,8 +1,9 @@
{% load common_tags %}
{% if object.consideracoesfinais.conteudo %}
<fieldset>
<p style="text-align: justify;margin-top: 0">
<strong>Considerações Finais: </strong>
{{object.consideracoesfinais.conteudo|striptags}}
{{object.consideracoesfinais.conteudo|sanitize|striptags}}
</p>
</fieldset>
{% endif %}

3
sapl/templates/sessao/blocos_ata/expedientes.html

@ -1,3 +1,4 @@
{% load common_tags %}
{% if expedientes %}
<fieldset>
<p style="text-align: justify;margin-top: 0">
@ -5,7 +6,7 @@
{% for e in expedientes %}
{% if e.conteudo %}
<b>{{ e.tipo }}</b>:
{{ e.conteudo|striptags }}
{{ e.conteudo|sanitize|striptags }}
{% endif %}
{% endfor %}
</p>

3
sapl/templates/sessao/blocos_ata/ocorrencias_da_sessao.html

@ -1,8 +1,9 @@
{% load common_tags %}
{% if object.ocorrenciasessao.conteudo %}
<fieldset>
<p style="text-align: justify;margin-top: 0">
<strong>Ocorrências da Sessão: </strong>
{{ object.ocorrenciasessao.conteudo|striptags }}
{{ object.ocorrenciasessao.conteudo|sanitize|striptags }}
</p>
</fieldset>
{% endif %}

Loading…
Cancel
Save