diff --git a/docassemble/AssemblyLine/al_document.py b/docassemble/AssemblyLine/al_document.py index fe72dfc9..35b9ba46 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 @@ -2474,23 +2485,23 @@ 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_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 +2571,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..da6cab34 100644 --- a/docassemble/AssemblyLine/test_al_document.py +++ b/docassemble/AssemblyLine/test_al_document.py @@ -1,8 +1,37 @@ # 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):