diff --git a/src/openflexure_microscope_server/ui.py b/src/openflexure_microscope_server/ui.py index 3cb4375e..f24726d3 100644 --- a/src/openflexure_microscope_server/ui.py +++ b/src/openflexure_microscope_server/ui.py @@ -1,11 +1,127 @@ """Functionality for communicating the required user interface for a thing.""" -from typing import Any, Optional +from html import escape +from html.parser import HTMLParser +from typing import Annotated, Any, Literal, Optional -from pydantic import BaseModel +from pydantic import BaseModel, BeforeValidator import labthings_fastapi as lt +ALLOWED_TAGS = { + "p", + "i", + "b", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "ul", + "li", + "a", +} + + +class SafeHTMLParser(HTMLParser): + """An HTMLParser that only allows a very limited subset of HTML.""" + + def __init__(self) -> None: + """Initialise the parser.""" + super().__init__() + self.output = "" + + def handle_starttag(self, tag: str, attrs: dict[str, str]) -> None: + """Append a start tag found in the HTML if it is in the allowed list. + + This is called by the base class when a start tag is found. We only append + the tag if it is our allowed list. We re-write the tag so no extra attributes + can be added. + + No tags can have attributes except for ``a`` tags only allows ``href``. We then + append ``target="_blank" rel="noopener noreferrer"`` to ensure the link opens + in a new window. + + :param tag: the tag name + :param attrs: the attributes for the tag. + """ + if tag not in ALLOWED_TAGS: + return + + if tag == "a": + href = None + for attr, value in attrs: + if attr == "href": + href = value.strip() + + if href: + safe_href = escape(href, quote=True) + # Always in a new tab + self.output.append( + f'' + ) + else: + self.output += "" + else: + self.output += f"<{tag}>" + + def handle_endtag(self, tag: str) -> None: + """Append the end tag only if it is in the allowed list.""" + if tag in ALLOWED_TAGS: + self.output += f"" + + def handle_startendtag(self, tag: str, _attrs: dict[str, str]) -> None: + """Append a self-closing tag if it is a new line.""" + if tag == "br": + self.output += "
" + + def handle_data(self, data: str) -> None: + """Append any data inside tags after escaping it.""" + self.output += escape(data) + + def handle_entityref(self, name: str) -> None: + """Append any named character.""" + self.output += f"&{name};" + + def handle_charref(self, name: str) -> None: + """Append any character references.""" + self.output += f"&#{name};" + + +def sanitize_html(html: str) -> str: + """Santitise HTML to only have a small list of allowed tags. + + Tags allowed without attrs: ``

, , ,

,

,

,

,

,
,`` + ``