This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new 0db2a30308 TIKA-4992: follow up Tika gui html view test (#3401)
0db2a30308 is described below

commit 0db2a30308d79ec67be5f30c0274a1a406eb1513
Author: Tim Allison <[email protected]>
AuthorDate: Thu Oct 22 16:10:39 2026 -0400

    TIKA-4992: follow up Tika gui html view test (#3401)
    
    * TIKA-4992: regression test for the TikaGUI html view
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 CHANGES.txt                                        |   4 +
 .../src/main/java/org/apache/tika/gui/TikaGUI.java | 103 ++++++----
 .../org/apache/tika/gui/TikaGUIHtmlViewTest.java   | 217 +++++++++++++++++++++
 3 files changed, 287 insertions(+), 37 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 0371aec9c4..7399f40d50 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -93,6 +93,10 @@ Release 4.2.0 - 10/22/2026
      document referenced, which Tika never resolves (TIKA-4941). XFA is read 
with SAX;
      XMLReaderUtils.getXMLInputFactory() is deprecated (TIKA-4938).
 
+   * The tika-app GUI's Formatted Text view no longer lets a document make 
Swing
+     construct classes or fetch URLs: only plain elements and attributes reach 
it, and
+     remote images are not loaded (TIKA-4992).
+
   SERVER, PIPES AND OPERATIONS
 
    * The file-system emitter can gzip its output ("compression": "GZIP"); 
tika-eval reads
diff --git a/tika-app/src/main/java/org/apache/tika/gui/TikaGUI.java 
b/tika-app/src/main/java/org/apache/tika/gui/TikaGUI.java
index 00cfe41fe8..aa4d9d9e68 100644
--- a/tika-app/src/main/java/org/apache/tika/gui/TikaGUI.java
+++ b/tika-app/src/main/java/org/apache/tika/gui/TikaGUI.java
@@ -41,6 +41,7 @@ import java.nio.file.StandardCopyOption;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import javax.swing.Box;
@@ -105,8 +106,14 @@ public class TikaGUI extends JFrame implements 
ActionListener, HyperlinkListener
     //maximum length to allow for mark for reparse to get JSON
     private static final int MAX_MARK = 20 * 1024 * 1024;//20MB
 
-    // Swing's html view instantiates the class an <object> names and fetches 
<link> stylesheets
-    private static final Set<String> HTML_VIEW_DROPPED = Set.of("head", 
"link", "object", "param");
+    // Swing's html view builds Components from <object> and fetches frame, 
stylesheet, image
+    // and background URLs, so only plain markup and attributes reach it
+    private static final Set<String> HTML_VIEW_ELEMENTS = Set.of("html", 
"body", "p", "div", "span", "br",
+            "hr", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "blockquote", 
"q", "address", "ul", "ol", "li",
+            "dl", "dt", "dd", "table", "caption", "thead", "tbody", "tfoot", 
"tr", "th", "td", "a", "img", "b",
+            "i", "u", "em", "strong", "sub", "sup", "code", "ins", "del");
+    private static final Set<String> HTML_VIEW_ATTRIBUTES = Set.of("alt", 
"title", "href", "name", "id",
+            "colspan", "rowspan");
 
     /**
      * Serial version UID.
@@ -337,7 +344,8 @@ public class TikaGUI extends JFrame implements 
ActionListener, HyperlinkListener
         StringBuilder metadataBuffer = new StringBuilder();
 
         ContentHandler handler =
-                new TeeContentHandler(getHtmlHandler(htmlBuffer), 
getTextContentHandler(textBuffer), getTextMainContentHandler(textMainBuffer), 
getXmlContentHandler(xmlBuffer));
+                new TeeContentHandler(getHtmlHandler(htmlBuffer, imageParser), 
getTextContentHandler(textBuffer),
+                        getTextMainContentHandler(textMainBuffer), 
getXmlContentHandler(xmlBuffer));
 
         context.set(DocumentSelector.class, new ImageDocumentSelector());
 
@@ -505,8 +513,10 @@ public class TikaGUI extends JFrame implements 
ActionListener, HyperlinkListener
      * to the given writer. The XHTML namespace is removed from the output
      * to prevent the serializer from using the &lt;tag/&gt; empty element
      * syntax that causes extra "&gt;" characters to be displayed.
-     * The &lt;head&gt; tags are dropped to prevent the serializer from
-     * generating a &lt;META&gt; content type tag that makes
+     * Elements and attributes outside a small allowlist are dropped (element
+     * text is kept), so {@link JEditorPane} has nothing to fetch or
+     * instantiate. This also drops &lt;head&gt;, which would make the
+     * serializer generate a &lt;META&gt; content type tag that makes
      * {@link JEditorPane} fail thinking that the document character set
      * is inconsistent.
      * <p>
@@ -514,10 +524,11 @@ public class TikaGUI extends JFrame implements 
ActionListener, HyperlinkListener
      * image links to be file:///(temporary file) so that they can be loaded.
      *
      * @param writer output writer
+     * @param imageParser saves the embedded images the view references
      * @return HTML content handler
      * @throws TransformerConfigurationException if an error occurs
      */
-    private ContentHandler getHtmlHandler(Writer writer) throws 
TransformerConfigurationException, TikaException {
+    static ContentHandler getHtmlHandler(Writer writer, ImageSavingParser 
imageParser) throws TransformerConfigurationException, TikaException {
         SAXTransformerFactory factory = 
XMLReaderUtils.getSAXTransformerFactory();
         TransformerHandler handler = factory.newTransformerHandler();
         handler
@@ -530,38 +541,33 @@ public class TikaGUI extends JFrame implements 
ActionListener, HyperlinkListener
                 if (XHTMLContentHandler.XHTML.equals(uri)) {
                     uri = null;
                 }
-                if (!HTML_VIEW_DROPPED.contains(localName)) {
-                    if ("img".equals(localName)) {
-                        // a copy: the other tee'd handlers see the same 
attributes
-                        AttributesImpl newAttrs = new AttributesImpl(atts);
-
-                        for (int i = 0; i < newAttrs.getLength(); i++) {
-                            if ("src".equals(newAttrs.getLocalName(i))) {
-                                String src = newAttrs.getValue(i);
-                                if (src.startsWith("embedded:")) {
-                                    String filename = 
src.substring(src.indexOf(':') + 1);
-                                    try {
-                                        File img = 
imageParser.requestSave(filename);
-                                        String newSrc = img
-                                                .toURI()
-                                                .toString();
-                                        newAttrs.setValue(i, newSrc);
-                                    } catch (IOException e) {
-                                        System.err.println("Error creating 
temp image file " + filename);
-                                        // The html viewer will show a broken 
image too to alert them
-                                    }
-                                } else {
-                                    // the view would fetch it on display
-                                    newAttrs.removeAttribute(i);
-                                    i--;
-                                }
+                if (!isAllowed(HTML_VIEW_ELEMENTS, localName, name)) {
+                    return;
+                }
+                boolean img = isAllowed(Set.of("img"), localName, name);
+                // a copy: the other tee'd handlers see the same attributes
+                AttributesImpl newAttrs = new AttributesImpl();
+                for (int i = 0; i < atts.getLength(); i++) {
+                    String attrLocal = atts.getLocalName(i);
+                    String attrQName = atts.getQName(i);
+                    if (img && isAllowed(Set.of("src"), attrLocal, attrQName)) 
{
+                        String src = atts.getValue(i);
+                        if (src.startsWith("embedded:")) {
+                            String filename = src.substring(src.indexOf(':') + 
1);
+                            try {
+                                File imgFile = 
imageParser.requestSave(filename);
+                                newAttrs.addAttribute(atts.getURI(i), 
attrLocal, attrQName, atts.getType(i),
+                                        imgFile.toURI().toString());
+                            } catch (IOException e) {
+                                System.err.println("Error creating temp image 
file " + filename);
+                                // The html viewer will show a broken image 
too to alert them
                             }
                         }
-                        super.startElement(uri, localName, name, newAttrs);
-                    } else {
-                        super.startElement(uri, localName, name, atts);
+                    } else if (isAllowed(HTML_VIEW_ATTRIBUTES, attrLocal, 
attrQName)) {
+                        newAttrs.addAttribute(atts.getURI(i), attrLocal, 
attrQName, atts.getType(i), atts.getValue(i));
                     }
                 }
+                super.startElement(uri, localName, name, newAttrs);
             }
 
             @Override
@@ -569,11 +575,20 @@ public class TikaGUI extends JFrame implements 
ActionListener, HyperlinkListener
                 if (XHTMLContentHandler.XHTML.equals(uri)) {
                     uri = null;
                 }
-                if (!HTML_VIEW_DROPPED.contains(localName)) {
+                if (isAllowed(HTML_VIEW_ELEMENTS, localName, name)) {
                     super.endElement(uri, localName, name);
                 }
             }
 
+            // the serializer writes the data raw and Swing ends a PI at the 
first '>'
+            @Override
+            public void processingInstruction(String target, String data) {
+            }
+
+            @Override
+            public void skippedEntity(String name) {
+            }
+
             @Override
             public void startPrefixMapping(String prefix, String uri) {
             }
@@ -584,6 +599,20 @@ public class TikaGUI extends JFrame implements 
ActionListener, HyperlinkListener
         };
     }
 
+    // the serializer writes the qName and Swing ignores case, so both names 
must pass
+    private static boolean isAllowed(Set<String> allowed, String localName, 
String qName) {
+        boolean named = false;
+        for (String n : new String[]{localName, qName}) {
+            if (n != null && !n.isEmpty()) {
+                if (!allowed.contains(n.toLowerCase(Locale.ROOT))) {
+                    return false;
+                }
+                named = true;
+            }
+        }
+        return named;
+    }
+
     private ContentHandler getTextContentHandler(Writer writer) {
         return new BodyContentHandler(writer);
     }
@@ -617,12 +646,12 @@ public class TikaGUI extends JFrame implements 
ActionListener, HyperlinkListener
      * directory, and delegates everything else to another downstream
      * parser.
      */
-    private static class ImageSavingParser implements Parser {
+    static class ImageSavingParser implements Parser {
         private Map<String, File> wanted = new HashMap<>();
         private Parser downstreamParser;
         private File tmpDir;
 
-        private ImageSavingParser(Parser downstreamParser) {
+        ImageSavingParser(Parser downstreamParser) {
             this.downstreamParser = downstreamParser;
 
             try {
diff --git 
a/tika-app/src/test/java/org/apache/tika/gui/TikaGUIHtmlViewTest.java 
b/tika-app/src/test/java/org/apache/tika/gui/TikaGUIHtmlViewTest.java
new file mode 100644
index 0000000000..7937919474
--- /dev/null
+++ b/tika-app/src/test/java/org/apache/tika/gui/TikaGUIHtmlViewTest.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.gui;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.swing.JEditorPane;
+import javax.swing.SwingUtilities;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.helpers.AttributesImpl;
+
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.AutoDetectParser;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.html.HtmlMapper;
+import org.apache.tika.parser.html.IdentityHtmlMapper;
+import org.apache.tika.sax.TeeContentHandler;
+import org.apache.tika.sax.ToXMLContentHandler;
+import org.apache.tika.sax.XHTMLContentHandler;
+
+public class TikaGUIHtmlViewTest {
+
+    private static final String HTML = "<html><head><title>t</title>"
+            + "<link rel=\"stylesheet\" 
href=\"http://attacker.invalid/head.css\";></head><body>"
+            + "<p>before</p>"
+            + "<link rel=\"stylesheet\" 
href=\"http://attacker.invalid/body.css\";>"
+            + "<object classid=\"javax.swing.JLabel\"><param name=\"text\" 
value=\"evil\"></object>"
+            + "<img src=\"http://attacker.invalid/remote.png\"; alt=\"remote\">"
+            + "<img src=\"embedded:image1.png\" alt=\"local\">"
+            + "<p>after</p></body></html>";
+
+    // every construct here makes Swing's html view request BASE or build a 
Component
+    private static final String ACTIVE = "<html><head><link rel=\"stylesheet\" 
href=\"BASE/link.css\"></head>"
+            + "<body background=\"BASE/body-bg\"><p 
style=\"background-image:url(BASE/style)\">styled</p>"
+            + "<table background=\"BASE/table-bg\"><tr><td 
background=\"BASE/td-bg\">cell</td></tr></table>"
+            + "<input type=\"image\" src=\"BASE/input\">"
+            + "<img src=\"BASE/img.png\" alt=\"remote\">"
+            + "<object classid=\"javax.swing.JLabel\"><param name=\"text\" 
value=\"evil\"></object>"
+            + "<frameset rows=\"50%,50%\"><frame 
src=\"BASE/frame\"></frameset>"
+            + "<p>visible</p></body></html>";
+
+    @Test
+    public void testHtmlViewDropsActiveContent() throws Exception {
+        StringWriter view = new StringWriter();
+        ToXMLContentHandler xhtml = new ToXMLContentHandler();
+        TeeContentHandler handler = new TeeContentHandler(
+                TikaGUI.getHtmlHandler(view, new 
TikaGUI.ImageSavingParser(null)), xhtml);
+        Metadata metadata = new Metadata();
+        try (TikaInputStream tis = TikaInputStream.get(HTML.getBytes(UTF_8))) {
+            new AutoDetectParser().parse(tis, handler, metadata, new 
ParseContext());
+        }
+        String html = view.toString();
+        try {
+            assertTrue(html.contains("before") && html.contains("after"), 
html);
+            assertFalse(html.contains("<object"), html);
+            assertFalse(html.contains("classid"), html);
+            assertFalse(html.contains("<param"), html);
+            assertFalse(html.contains("<link"), html);
+            assertFalse(html.contains("attacker.invalid"), html);
+            assertTrue(html.contains("alt=\"remote\""), html);
+            assertTrue(html.contains("src=\"file:"), html);
+
+            // the tee'd handlers still see the parser's own attributes
+            String xml = xhtml.toString();
+            
assertTrue(xml.contains("src=\"http://attacker.invalid/remote.png\"";), xml);
+            assertTrue(xml.contains("src=\"embedded:image1.png\""), xml);
+        } finally {
+            deleteSavedImages(html);
+        }
+    }
+
+    @Test
+    public void testHtmlViewMatchesSerializedName() throws Exception {
+        StringWriter view = new StringWriter();
+        ContentHandler handler = TikaGUI.getHtmlHandler(view, new 
TikaGUI.ImageSavingParser(null));
+        AttributesImpl objectAtts = new AttributesImpl();
+        objectAtts.addAttribute("", "", "classid", "CDATA", 
"javax.swing.JLabel");
+        AttributesImpl imgAtts = new AttributesImpl();
+        imgAtts.addAttribute("", "", "SRC", "CDATA", 
"http://attacker.invalid/remote.png";);
+        handler.startDocument();
+        handler.startElement(XHTMLContentHandler.XHTML, "body", "body", new 
AttributesImpl());
+        handler.startElement("", "", "OBJECT", objectAtts);
+        handler.endElement("", "", "OBJECT");
+        handler.startElement("", "", "IMG", imgAtts);
+        handler.endElement("", "", "IMG");
+        handler.processingInstruction("x", "a><object 
classid=\"javax.swing.JLabel\"></object><x ");
+        char[] text = "<object classid=\"javax.swing.JLabel\">".toCharArray();
+        handler.characters(text, 0, text.length);
+        handler.endElement(XHTMLContentHandler.XHTML, "body", "body");
+        handler.endDocument();
+        String html = view.toString();
+        assertFalse(html.toLowerCase(Locale.ROOT).contains("<object"), html);
+        assertTrue(html.contains("&lt;object"), html);
+        assertFalse(html.contains("attacker.invalid"), html);
+        assertTrue(html.contains("<IMG"), html);
+    }
+
+    @Test
+    public void testSwingRendersNothingActive() throws Exception {
+        // calibration: the unfiltered markup does reach the listener and 
build a Component
+        try (Listener raw = new Listener()) {
+            JEditorPane pane = render(ACTIVE.replace("BASE", raw.base()));
+            raw.awaitAtLeast(5);
+            assertTrue(pane.getComponentCount() > 0);
+        }
+
+        // IdentityHtmlMapper passes through whatever the document has, as a 
custom parser could
+        ParseContext context = new ParseContext();
+        context.set(HtmlMapper.class, IdentityHtmlMapper.INSTANCE);
+        try (Listener filtered = new Listener()) {
+            StringWriter view = new StringWriter();
+            try (TikaInputStream tis = 
TikaInputStream.get(ACTIVE.replace("BASE", filtered.base()).getBytes(UTF_8))) {
+                new AutoDetectParser().parse(tis, TikaGUI.getHtmlHandler(view, 
new TikaGUI.ImageSavingParser(null)),
+                        new Metadata(), context);
+            }
+            String html = view.toString();
+            assertTrue(html.contains("visible"), html);
+            JEditorPane pane = render(html);
+            Thread.sleep(1000);
+            assertEquals(List.of(), filtered.requests, html);
+            assertEquals(0, pane.getComponentCount(), html);
+        }
+    }
+
+    private static JEditorPane render(String html) throws Exception {
+        JEditorPane[] pane = new JEditorPane[1];
+        SwingUtilities.invokeAndWait(() -> {
+            pane[0] = new JEditorPane();
+            pane[0].setContentType("text/html");
+            pane[0].setText(html);
+            pane[0].setSize(800, 600);
+            pane[0].getPreferredSize();
+        });
+        return pane[0];
+    }
+
+    private static void deleteSavedImages(String html) throws Exception {
+        Matcher m = Pattern.compile("src=\"(file:[^\"]+)\"").matcher(html);
+        while (m.find()) {
+            Files.deleteIfExists(Path.of(URI.create(m.group(1))));
+        }
+    }
+
+    private static class Listener implements AutoCloseable {
+        private final ServerSocket server = new ServerSocket(0, 50, 
InetAddress.getLoopbackAddress());
+        private final List<String> requests = new CopyOnWriteArrayList<>();
+
+        Listener() throws Exception {
+            Thread t = new Thread(() -> {
+                while (true) {
+                    try (Socket s = server.accept()) {
+                        requests.add(new BufferedReader(new 
InputStreamReader(s.getInputStream(), UTF_8)).readLine());
+                        s.getOutputStream().write("HTTP/1.0 404 Not 
Found\r\nContent-Length: 0\r\n\r\n".getBytes(UTF_8));
+                    } catch (SocketException e) {
+                        return;
+                    } catch (Exception e) {
+                        //swallow
+                    }
+                }
+            });
+            t.setDaemon(true);
+            t.start();
+        }
+
+        String base() {
+            return "http://127.0.0.1:"; + server.getLocalPort();
+        }
+
+        void awaitAtLeast(int n) throws InterruptedException {
+            long deadline = System.currentTimeMillis() + 10_000;
+            while (requests.size() < n && System.currentTimeMillis() < 
deadline) {
+                Thread.sleep(50);
+            }
+            assertTrue(requests.size() >= n, requests.toString());
+        }
+
+        @Override
+        public void close() throws Exception {
+            server.close();
+        }
+    }
+}

Reply via email to