From 67bad01aca45375b653eac3ddfbeec44002eae20 Mon Sep 17 00:00:00 2001 From: Christian Isenberg Date: Thu, 13 Aug 2026 15:50:26 +0200 Subject: [PATCH 1/5] Translated using Weblate (Portuguese (Brazil)) Currently translated at 49.2% (409 of 831 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/pt_BR/ --- front/php/templates/language/pt_br.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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": "", From 208fa928ea50dc4f58a31d4b1b9a1481031d7935 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:26:47 +0000 Subject: [PATCH 2/5] Initial plan From 0197e7c2cfd7d3b908dfc72c8363da63e797cbb2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:29:44 +0000 Subject: [PATCH 3/5] fix: escape notification HTML device fields Co-authored-by: jokob-sk <96159884+jokob-sk@users.noreply.github.com> --- server/models/notification_instance.py | 57 +++++++++++++++------ test/backend/test_notification_templates.py | 50 ++++++++++++++++++ 2 files changed, 92 insertions(+), 15 deletions(-) diff --git a/server/models/notification_instance.py b/server/models/notification_instance.py index 4423a5064..9a20b34c9 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,43 @@ def format_table(html, thValue, props, newThValue=""): ) +# ----------------------------------------------------------------------------- +# Escape free-text values before embedding them into notification HTML +def escape_html_rows(rows): + 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): + # 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 = html.escape(" • ".join(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( + "warn", + 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..3a1614ada 100644 --- a/test/backend/test_notification_templates.py +++ b/test/backend/test_notification_templates.py @@ -295,6 +295,56 @@ 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 ", html) + self.assertIn("Meta Quest PREHEADERbroken < content", + ["Meta Quest Date: Fri, 14 Aug 2026 15:34:07 +0000 Subject: [PATCH 4/5] test: cover escaped notification html fallback Co-authored-by: jokob-sk <96159884+jokob-sk@users.noreply.github.com> --- server/models/notification_instance.py | 4 +++- test/backend/test_notification_templates.py | 25 +++++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/server/models/notification_instance.py b/server/models/notification_instance.py index 9a20b34c9..2f78e9877 100755 --- a/server/models/notification_instance.py +++ b/server/models/notification_instance.py @@ -391,6 +391,7 @@ 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 @@ -403,9 +404,10 @@ def escape_html_rows(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 = html.escape(" • ".join(preheaders)) + preheader = " • ".join(html.escape(entry) for entry in preheaders) padding = (" ‌ " * 47) mail_html = mail_html.replace( diff --git a/test/backend/test_notification_templates.py b/test/backend/test_notification_templates.py index 3a1614ada..6f36c5cfd 100644 --- a/test/backend/test_notification_templates.py +++ b/test/backend/test_notification_templates.py @@ -327,23 +327,30 @@ def test_html_escapes_free_text_values(self, mock_setting): self.assertIn("Meta Quest <Pro", html) self.assertIn("values <=2 break things", html) self.assertNotIn("Meta Quest ", html) + self.assertNotIn("values <=2 break things", html) self.assertIn("Meta Quest PREHEADERbroken < content", - ["Meta Quest Date: Fri, 14 Aug 2026 23:51:15 +0000 Subject: [PATCH 5/5] fix: use valid notification fallback log level Co-authored-by: jokob-sk <96159884+jokob-sk@users.noreply.github.com> --- .github/skills/code-standards/SKILL.md | 1 + server/models/notification_instance.py | 2 +- test/backend/test_notification_templates.py | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) 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/server/models/notification_instance.py b/server/models/notification_instance.py index 2f78e9877..45327659f 100755 --- a/server/models/notification_instance.py +++ b/server/models/notification_instance.py @@ -421,7 +421,7 @@ def finalize_html(mail_html, preheaders): ) except XMLTokenError as err: mylog( - "warn", + "none", f"[Notification] Failed to pretty-print HTML report, sending unindented HTML instead: {err}", ) return mail_html diff --git a/test/backend/test_notification_templates.py b/test/backend/test_notification_templates.py index 6f36c5cfd..a21e6ece7 100644 --- a/test/backend/test_notification_templates.py +++ b/test/backend/test_notification_templates.py @@ -334,8 +334,9 @@ def test_html_escapes_free_text_values(self, mock_setting): # ----------------------------------------------------------------- # Final HTML escapes preheaders and tolerates indent failures # ----------------------------------------------------------------- + @patch("models.notification_instance.mylog") @patch("models.notification_instance.indent") - def test_finalize_html_escapes_preheader_and_falls_back(self, mock_indent): + def test_finalize_html_escapes_preheader_and_falls_back(self, mock_indent, mock_mylog): from models.notification_instance import finalize_html, XMLTokenError mock_indent.side_effect = XMLTokenError("broken html") @@ -351,6 +352,10 @@ def test_finalize_html_escapes_preheader_and_falls_back(self, mock_indent): self.assertEqual(final_html, expected_html) mock_indent.assert_called_once() + mock_mylog.assert_called_once_with( + "none", + "[Notification] Failed to pretty-print HTML report, sending unindented HTML instead: broken html", + ) if __name__ == "__main__":