7a356d85dba1647c6d918b315e075da7e31ab7cb
braney
  Sat Sep 26 12:10:36 2026 -0700
htmlSanitize: keep simple svg drawings in hub description pages, refs #38428

Description pages that draw small svg shapes, such as a colour legend, came
out empty.  svg, g, circle, ellipse, rect, line, polyline, polygon and path
now come through with their size, position, fill and stroke attributes.
Other svg elements are left out.

Over the 5390-page hub corpus only the 32 pages that contain an svg
change, and no page's text changes.

diff --git src/lib/tests/htmlSanitizeTest.c src/lib/tests/htmlSanitizeTest.c
index cd081f855cd..6d4595e3670 100644
--- src/lib/tests/htmlSanitizeTest.c
+++ src/lib/tests/htmlSanitizeTest.c
@@ -25,56 +25,77 @@
 /* ordinary links are left alone, and a new window does not get a handle on ours */
 "<a href=\"https://genome.ucsc.edu\">u</a> <a href=\"#anchor\" target=\"_blank\">a</a>",
 /* an image keeps its source, not its onerror */
 "<img src=\"pic.png\" alt=\"a\" onerror=\"alert(1)\" width=\"20\">",
 /* a frame survives only when it plays a video from a host we know */
 "<iframe width=\"560\" src=\"https://www.youtube.com/embed/abc\"></iframe>"
     "<iframe src=\"https://example.com/x\">fallback</iframe>",
 /* the style attribute is filtered a property at a time */
 "<p style=\"color:red;behavior:url(#default#VML);text-align:center;position:fixed\">p</p>",
 /* unknown elements lose their tag and keep their text */
 "<o:p>word</o:p><vertebrates>more</vertebrates>",
 /* tags left open are closed for us, and a slash does not close one of these */
 "<div><b>bold<p>para",
 "<div style=\"color:red\"/>the rest of the page is not inside that div",
 /* a page built of tags that are never closed is not a way to make us work all day */
-"<svg><svg><svg><svg>text",
+"<object><object><object><object>text",
 /* a stray quote inside an unquoted value does not swallow the page */
 "<a href=https://example.com/x\\\">link text</a> and more text",
 /* a form and everything in it goes */
 "<form action=\"/x\"><input name=\"password\"><button>Log in</button></form><p>after</p>",
 /* comments and doctypes go */
 "<!DOCTYPE html><!-- <p>hidden</p> --><p>shown</p>",
 /* an id, and a name on an anchor, are renamed, and a link to one of them is renamed too */
 "<h2 id=\"methods\">M</h2><a name=\"top\">t</a>"
     "<a href=\"#methods\">same page</a> <a href=\"other.html#methods\">other page</a>"
     "<a href=\"#\">to the top</a><div id=\"\">no name at all</div>",
 /* a table keeps its shape */
 "<table border=\"1\"><tr><td colspan=\"2\" bgcolor=\"#eee\">cell</td></tr></table>",
 /* an entity in a style value cannot spell a property value we would not print */
 "<p style=\"list-style:u&#114l(http://example.com/x.png);color:re&#100\">a</p>",
 /* an ampersand the author meant still reads as one */
 "<p style=\"font-family:'AT&T Sans'\">a</p>",
 /* the same attribute twice is written once, the way a browser reads it */
 "<img src=\"first.png\" src=\"second.png\" alt=\"a\" alt=\"b\">"
     "<a href=\"javascript:alert(1)\" href=\"https://example.com\">x</a>",
 /* a quote that is never closed takes the rest of the page, and we say so */
 "<p>real</p><p title=\"oops>never printed</p>",
 /* a less than sign that starts no tag is text, and cannot eat a tag of ours */
 "<div>a &lt; b</ <b>bold</b></div>",
 /* a name we already renamed is not renamed again, on either side of the link */
 "<h2 id=\"descPage-methods\">M</h2><a href=\"#descPage-methods\">jump</a>",
+/* a colored circle in a legend survives, the way UniBind draws one */
+"<td><svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 2 2\" "
+    "width=\"2em\"><g>\n<circle cx=\"1\" cy=\"1\" r=\"1\" style=\"fill: rgb(209,213,23)\" />"
+    "</g></svg></td>",
+/* shapes written with a closing slash are siblings, not each inside the one before */
+"<svg><path d=\"M0 0L1 1\"/><rect x=\"1\" y=\"1\" width=\"2\" height=\"2\"/>"
+    "<circle r=\"1\"></circle></svg>",
+/* nothing in a drawing may run, link, load or animate */
+"<svg onload=\"alert(1)\"><script>alert(1)</script>"
+    "<a href=\"javascript:alert(1)\"><rect width=\"10\" height=\"10\"/></a>"
+    "<use href=\"#x\"/><animate attributeName=\"href\" to=\"javascript:alert(1)\"/>"
+    "<image href=\"https://example.com/x.png\"/>"
+    "<foreignObject><p>text</p></foreignObject></svg>",
+/* a fill or a stroke cannot fetch anything, however the url is spelled */
+"<svg><circle r=\"1\" fill=\"url(https://example.com/x.svg#p)\"/>"
+    "<rect stroke=\"u&#114;l(https://example.com/y)\" fill=\"#c00\"/></svg>",
+/* a shape that only clips another is not drawn on its own */
+"<svg><defs><clipPath id=\"c\"><path d=\"M0 0L9 9\"/></clipPath></defs>"
+    "<rect width=\"9\" height=\"9\"/></svg>",
+/* a shape outside a drawing loses its tag */
+"<circle r=\"5\"/>text after",
 };
 
 int main(int argc, char *argv[])
 {
 int i;
 for (i = 0;  i < ArraySize(cases);  ++i)
     {
     char *clean = htmlSanitize(cases[i]);
     printf("in : %s\nout: %s\n", cases[i], clean);
     struct slName *removed = NULL, *el;
     freeMem(clean);
     clean = htmlSanitizeReport(cases[i], &removed);
     for (el = removed;  el != NULL;  el = el->next)
         printf("     (%s)\n", el->name);
     printf("\n");