From b0a6394abfacc11fdbd59f62a301c2e21772095c Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Sun, 19 Jul 2026 11:45:21 -0400 Subject: [PATCH 1/2] Fix #1079 - issue with escaping dictionary keys in send_button_html --- docassemble/AssemblyLine/al_document.py | 75 ++++++++++---------- docassemble/AssemblyLine/test_al_document.py | 33 ++++++++- 2 files changed, 71 insertions(+), 37 deletions(-) diff --git a/docassemble/AssemblyLine/al_document.py b/docassemble/AssemblyLine/al_document.py index fe72dfc9..2867081f 100644 --- a/docassemble/AssemblyLine/al_document.py +++ b/docassemble/AssemblyLine/al_document.py @@ -1,6 +1,8 @@ -import re -import os +import json import mimetypes +import os +import re +from html import escape from typing import Any, Dict, List, Literal, Union, Callable, Optional from docassemble.base.util import ( Address, @@ -175,6 +177,14 @@ def html_safe_str(the_string: str) -> str: return re.sub(r"[^A-Za-z0-9]+", "_", the_string) +def _javascript_href(function_name: str, *args: Any) -> str: + """Build a JavaScript URL that is safe to use in an HTML href attribute.""" + return escape( + f"javascript:{function_name}({','.join(json.dumps(arg) for arg in args)})", + quote=True, + ) + + def table_row(title: str, button_htmls: List[str] = []) -> str: """ Generate an HTML row string for an AL document-styled table. @@ -2408,10 +2418,11 @@ def send_email_table_row( al_email_input_id = "_ignore_al_doc_email_" + name al_send_button_id = "al_send_email_button_" + name - javascript_string = ( - f"javascript:aldocument_send_action(" - f"'{self.attr_name('send_email_action_event')}'," - f"'{al_wants_editable_input_id}','{al_email_input_id}')" + javascript_string = _javascript_href( + "aldocument_send_action", + str(self.attr_name("send_email_action_event")), + al_wants_editable_input_id, + al_email_input_id, ) # Label "email" and input field for the 1st column of the table row @@ -2475,22 +2486,21 @@ def send_button_to_html( al_send_button_id = "al_send_email_to_button_" + name if isinstance(preferred_formats, (list, tuple)): - formats_js = "[" + ",".join(f"'{fmt}'" for fmt in preferred_formats) + "]" + formats_value = [str(fmt) for fmt in preferred_formats] elif preferred_formats: - formats_js = f"'{preferred_formats}'" + formats_value = str(preferred_formats) else: - formats_js = "null" - - javascript_string = ( - f"javascript:aldocument_send_to_action(" - f"'{self.attr_name('send_email_to_action_event')}'," - f"'{editable}'," - f"'{email}'," - f"'{al_send_button_id}'," - f"'{template_name}'," - f"'{key}'," - f"{formats_js}" - f")" + formats_value = None + + javascript_string = _javascript_href( + "aldocument_send_to_action", + str(self.attr_name("send_email_to_action_event")), + str(editable), + str(email), + al_send_button_id, + str(template_name), + str(key), + formats_value, ) send_button = action_button_html( javascript_string, @@ -2560,22 +2570,15 @@ def send_button_html( al_email_input_id = "_ignore_al_doc_email_" + name al_send_button_id = "al_send_email_button_" + name - if isinstance(preferred_formats, (list, tuple)): - formats_js = "[" + ",".join(f"'{fmt}'" for fmt in preferred_formats) + "]" - elif preferred_formats: - formats_js = f"'{preferred_formats}'" - else: - formats_js = "null" - - javascript_string = ( - f"javascript:aldocument_send_action(" - f"'{self.attr_name('send_email_action_event')}'," - f"'{al_wants_editable_input_id}'," - f"'{al_email_input_id}'," - f"'{template_name}'," - f"'{key}'," - f"{formats_js}" - ")" + formats_value = [str(fmt) for fmt in preferred_formats] + javascript_string = _javascript_href( + "aldocument_send_action", + str(self.attr_name("send_email_action_event")), + al_wants_editable_input_id, + al_email_input_id, + str(template_name), + str(key), + formats_value, ) # Container of whole email section with header diff --git a/docassemble/AssemblyLine/test_al_document.py b/docassemble/AssemblyLine/test_al_document.py index f8a1c063..78c3d57d 100644 --- a/docassemble/AssemblyLine/test_al_document.py +++ b/docassemble/AssemblyLine/test_al_document.py @@ -1,8 +1,39 @@ # do not pre-load +import json import unittest +from html import unescape from docassemble.base.util import DAFile, DATemplate -from .al_document import ALDocument, ALDocumentBundle, ALAddendumField +from .al_document import ALDocument, ALDocumentBundle, ALAddendumField, _javascript_href + + +class TestJavascriptHref(unittest.TestCase): + def test_escapes_javascript_strings_inside_html_href(self): + result = _javascript_href( + "aldocument_send_action", + "template_request.requestee_bundles['4167e36be7f04794ad30770e865afe68']", + "_ignore_al_wants_editable_bundle", + "_ignore_al_doc_email_bundle", + ) + + self.assertEqual( + result, + "javascript:aldocument_send_action(" + ""template_request.requestee_bundles['4167e36be7f04794ad30770e865afe68']"," + ""_ignore_al_wants_editable_bundle"," + ""_ignore_al_doc_email_bundle")" + ) + + def test_preserves_javascript_value_types(self): + result = unescape( + _javascript_href("send", "None", None, ["pdf", "docx"]) + ) + args_json = result.removeprefix("javascript:send(").removesuffix(")") + + self.assertEqual( + json.loads(f"[{args_json}]"), + ["None", None, ["pdf", "docx"]], + ) class test_dont_assume_pdf(unittest.TestCase): From c3af5c1b9b83dd97d4449e314e6181f257ada022 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Sun, 19 Jul 2026 13:44:10 -0400 Subject: [PATCH 2/2] Fix mypy + black --- docassemble/AssemblyLine/al_document.py | 1 + docassemble/AssemblyLine/test_al_document.py | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docassemble/AssemblyLine/al_document.py b/docassemble/AssemblyLine/al_document.py index 2867081f..35b9ba46 100644 --- a/docassemble/AssemblyLine/al_document.py +++ b/docassemble/AssemblyLine/al_document.py @@ -2485,6 +2485,7 @@ def send_button_to_html( name = html_safe_str(self.instanceName) + random_suffix() al_send_button_id = "al_send_email_to_button_" + name + formats_value: Optional[Union[str, List[str]]] if isinstance(preferred_formats, (list, tuple)): formats_value = [str(fmt) for fmt in preferred_formats] elif preferred_formats: diff --git a/docassemble/AssemblyLine/test_al_document.py b/docassemble/AssemblyLine/test_al_document.py index 78c3d57d..da6cab34 100644 --- a/docassemble/AssemblyLine/test_al_document.py +++ b/docassemble/AssemblyLine/test_al_document.py @@ -21,13 +21,11 @@ def test_escapes_javascript_strings_inside_html_href(self): "javascript:aldocument_send_action(" ""template_request.requestee_bundles['4167e36be7f04794ad30770e865afe68']"," ""_ignore_al_wants_editable_bundle"," - ""_ignore_al_doc_email_bundle")" + ""_ignore_al_doc_email_bundle")", ) def test_preserves_javascript_value_types(self): - result = unescape( - _javascript_href("send", "None", None, ["pdf", "docx"]) - ) + result = unescape(_javascript_href("send", "None", None, ["pdf", "docx"])) args_json = result.removeprefix("javascript:send(").removesuffix(")") self.assertEqual(