diff --git a/.github/skills/code-standards/SKILL.md b/.github/skills/code-standards/SKILL.md index 83c52d0a8..e1f299bdf 100644 --- a/.github/skills/code-standards/SKILL.md +++ b/.github/skills/code-standards/SKILL.md @@ -24,6 +24,7 @@ description: NetAlertX coding standards and conventions. Use this when writing c - follow existing code style and structure, and ensure backward compatibility with existing installations when submitting PRs - all code needs to be scalable to handle large networks with thousands of devices (10k+) without performance degradation - no inline imports, all imports must be at the top of the file +- when using `server/logger.py` `mylog()`, only use valid levels: `none`, `minimal`, `verbose`, `debug`, `trace`; invalid levels silently degrade to `none` ## File Length diff --git a/front/php/templates/language/pt_br.json b/front/php/templates/language/pt_br.json index 22756ec92..b7fb0a5a9 100644 --- a/front/php/templates/language/pt_br.json +++ b/front/php/templates/language/pt_br.json @@ -207,13 +207,13 @@ "Device_MultiEdit_Backup": "Cuidado, inserir valores errados abaixo interromperá sua configuração. Faça backup do seu banco de dados ou da configuração dos dispositivos primeiro (clique para baixar ). Leia como recuperar dispositivos deste arquivo no Documentação de backups.", "Device_MultiEdit_Fields": "Editar campos:", "Device_MultiEdit_MassActions": "Ações em massa:", - "Device_MultiEdit_No_Devices": "", + "Device_MultiEdit_No_Devices": "Nenhum dispositivo selecionado.", "Device_MultiEdit_Tooltip": "Cuidadoso. Clicar aqui aplicará o valor à esquerda a todos os dispositivos selecionados acima.", "Device_NextScan_Imminent": "", - "Device_NextScan_In": "", + "Device_NextScan_In": "Próxima varredura em aproximadamente ", "Device_NoData_Help": "", - "Device_NoData_Scanning": "", - "Device_NoData_Title": "", + "Device_NoData_Scanning": "Aguarde a primeira varredura - isso pode levar vários minutos após a configuração inicial.", + "Device_NoData_Title": "Ainda não foram encontrados nenhum dispositivo", "Device_NoMatch_Title": "", "Device_Save_Failed": "", "Device_Save_Unauthorized": "", diff --git a/server/models/notification_instance.py b/server/models/notification_instance.py index 4423a5064..45327659f 100755 --- a/server/models/notification_instance.py +++ b/server/models/notification_instance.py @@ -1,8 +1,10 @@ +import html import json import re import uuid import socket from yattag import indent +from yattag.indentation import XMLTokenError from json2table import convert # Register NetAlertX modules @@ -146,20 +148,7 @@ def create(self, JSON, Extra=""): mail_html, conf.REPORT_DASHBOARD_URL + "/deviceDetails.php?mac=" ) - # Add preheader for inbox preview after all links have been generated. - # Invisible padding prevents email clients from showing the start of the email body. - preheader = " • ".join(preheaders) - - padding = (" ‌ " * 47) - - mail_html = mail_html.replace( - "PREHEADER", - preheader + padding, - ) - - final_html = indent( - mail_html, indentation=" ", newline="\r\n", indent_text=True - ) + final_html = finalize_html(mail_html, preheaders) send_api(self.JSON, final_text, final_html) @@ -335,8 +324,9 @@ def construct_notifications(JSON, section): text = tableTitle + "\n---------\n" # Convert a JSON into an HTML table + html_data = escape_html_rows(jsn) html = convert( - {"data": jsn}, + {"data": html_data}, build_direction=build_direction, table_attributes=table_attributes, ) @@ -398,6 +388,45 @@ def format_table(html, thValue, props, newThValue=""): ) +# ----------------------------------------------------------------------------- +# Escape free-text values before embedding them into notification HTML +def escape_html_rows(rows): + """Return a copy of notification rows with only string values HTML-escaped.""" + return [ + { + key: html.escape(value) if isinstance(value, str) else value + for key, value in row.items() + } + for row in rows + ] + + +# ----------------------------------------------------------------------------- +# Finalize HTML and tolerate pretty-print failures +def finalize_html(mail_html, preheaders): + """Insert an escaped preheader and pretty-print HTML, falling back to raw HTML on XML errors.""" + # Add preheader for inbox preview after all links have been generated. + # Invisible padding prevents email clients from showing the start of the email body. + preheader = " • ".join(html.escape(entry) for entry in preheaders) + padding = (" ‌ " * 47) + + mail_html = mail_html.replace( + "PREHEADER", + preheader + padding, + ) + + try: + return indent( + mail_html, indentation=" ", newline="\r\n", indent_text=True + ) + except XMLTokenError as err: + mylog( + "none", + f"[Notification] Failed to pretty-print HTML report, sending unindented HTML instead: {err}", + ) + return mail_html + + # ----------------------------------------------------------------------------- # Pre-header Preview def build_preheader(tableTitle, jsn, headers): diff --git a/test/backend/test_notification_templates.py b/test/backend/test_notification_templates.py index 1e8b8d9a1..a21e6ece7 100644 --- a/test/backend/test_notification_templates.py +++ b/test/backend/test_notification_templates.py @@ -295,6 +295,68 @@ def test_html_unchanged_with_template(self, mock_setting): self.assertEqual(html_without, html_with) + # ----------------------------------------------------------------- + # HTML output escapes free-text device values while text stays raw + # ----------------------------------------------------------------- + @patch("models.notification_instance.get_setting_value") + def test_html_escapes_free_text_values(self, mock_setting): + from models.notification_instance import construct_notifications + + mock_setting.side_effect = self._setting_factory({ + "NTFPRCS_TEXT_SECTION_HEADERS": True, + "NTFPRCS_TEXT_TEMPLATE_new_devices": "", + }) + + devices = [ + { + "devName": "Meta Quest