diff mbox series

[layerindex-web,3/4] layerindex/utils.py: fix sanitise_html() for Python 3 and current bs4

Message ID 20261113232808.96792-4-tim.orling@konsulko.com
State Under Review
Delegated to: Tim Orling
Headers show
Series Dependency upgrades, and a fix for site notices | expand

Commit Message

Tim Orling Nov. 13, 2026, 11:28 p.m. UTC
Site notices showed a literal b'...', because sanitise_html() returned
bytes, and a notice with an attribute on an allowed tag, such as
<p class="x">, failed to render, because current Beautiful Soup needs a
dict for tag.attrs. Return a str, clear attributes with a dict, and
move off the deprecated findAll() and renderContents(). Add tests.

AI-Generated: Claude Opus 5.5
Signed-off-by: Tim Orling <tim.orling@konsulko.com>
---
 layerindex/utils.py |  6 +++---
 tests/test_utils.py | 13 +++++++++++++
 2 files changed, 16 insertions(+), 3 deletions(-)
diff mbox series

Patch

diff --git a/layerindex/utils.py b/layerindex/utils.py
index fd29de0..8e06463 100644
--- a/layerindex/utils.py
+++ b/layerindex/utils.py
@@ -544,13 +544,13 @@  def run_command_interruptible(cmd):
 
 def sanitise_html(html):
     soup = BeautifulSoup(html, "html.parser")
-    for tag in soup.findAll(True):
+    for tag in soup.find_all(True):
         if tag.name not in ['strong', 'em', 'b', 'i', 'p', 'ul', 'ol', 'li', 'br', 'p']:
             tag.hidden = True
         elif tag.attrs:
-            tag.attrs = []
+            tag.attrs = {}
 
-    return soup.renderContents()
+    return soup.decode_contents()
 
 def squashspaces(string):
     return re.sub(r"\s+", " ", string).strip()
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 95b5e04..248b562 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -85,3 +85,16 @@  def test_in_nested_layer(repo_with_nested_layers, path, expected):
     assert utils.in_nested_layer(path, nested, repodir) is expected
     # Absolute paths give the same answer
     assert utils.in_nested_layer(os.path.join(repodir, path), nested, repodir) is expected
+
+
+@pytest.mark.parametrize('html, expected', [
+    # Allowed tags are kept, but lose their attributes
+    ('<p class="x" onclick="y">Hi <b>there</b></p>', '<p>Hi <b>there</b></p>'),
+    ('<ul><li>one</li></ul>', '<ul><li>one</li></ul>'),
+    # Other tags are dropped, but their text stays (escaped)
+    ('<a href="http://example.com">link</a>', 'link'),
+    ('<script>alert(1)</script>', 'alert(1)'),
+    ('<div><i>x</i> &lt;y&gt;</div>', '<i>x</i> &lt;y&gt;'),
+])
+def test_sanitise_html(html, expected):
+    assert utils.sanitise_html(html) == expected