From fdb4a6ed4fa92cb89b39c8e0da1f0e18226d043f Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Mon, 26 Oct 2026 13:51:49 +0100 Subject: [PATCH 1/4] JSPWIKI-1206 JSPWIKI-1221 Migrate from NekoHTML to htmlunit-neko org.codelibs:nekohtml is no longer maintained. Use org.htmlunit:neko-htmlunit, which needs Java 17 like the rest of the project. The new parser does not recognise the SAX validation feature that JDOM's XMLReaderSAX2Factory sets, and reports a null namespace URI for the attributes, which JDOM rejects. HtmlStringToWikiTranslator now gives JDOM its own reader factory, wrapping the parser in a filter that turns those URIs into empty strings. Element names are now lower case, as in XHTML, so the few hard coded upper case names in the translator are lower case too. jspwiki-main declared the dependency without using it, so it is dropped there. --- LICENSE | 2 +- jspwiki-main/pom.xml | 5 -- jspwiki-wysiwyg/pom.xml | 4 +- .../HtmlStringToWikiTranslator.java | 50 +++++++++++++++++-- .../XHtmlElementToWikiTranslator.java | 10 ++-- .../wiki/htmltowiki/syntax/FormDecorator.java | 2 +- pom.xml | 8 +-- 7 files changed, 58 insertions(+), 23 deletions(-) diff --git a/LICENSE b/LICENSE index cf15803cb5..4c5da67dad 100644 --- a/LICENSE +++ b/LICENSE @@ -270,7 +270,7 @@ lucene-queries-9.12.2.jar LICENSE lucene-queryparser-9.12.2.jar LICENSE lucene-sandbox-9.12.2.jar LICENSE javax.mail-1.6.2.jar ./jspwiki-war/src/main/config/doc/LICENSE.cddl -nekohtml-2.0.2.jar (codelibs fork) LICENSE +neko-htmlunit-5.6.0.jar LICENSE org.suigeneris.jrcs.diff-0.4.2 LICENSE oro-2.0.8.jar LICENSE sandler-0.5.jar ./jspwiki-war/src/main/config/doc/LICENSE.sandler diff --git a/jspwiki-main/pom.xml b/jspwiki-main/pom.xml index 5b5c43095c..a349cf1bce 100644 --- a/jspwiki-main/pom.xml +++ b/jspwiki-main/pom.xml @@ -157,11 +157,6 @@ lucene-queryparser - - org.codelibs - nekohtml - - org.freshcookies freshcookies-security diff --git a/jspwiki-wysiwyg/pom.xml b/jspwiki-wysiwyg/pom.xml index 3b5bc66f88..7dbe827f55 100644 --- a/jspwiki-wysiwyg/pom.xml +++ b/jspwiki-wysiwyg/pom.xml @@ -53,8 +53,8 @@ - org.codelibs - nekohtml + org.htmlunit + neko-htmlunit diff --git a/jspwiki-wysiwyg/src/main/java/org/apache/wiki/htmltowiki/HtmlStringToWikiTranslator.java b/jspwiki-wysiwyg/src/main/java/org/apache/wiki/htmltowiki/HtmlStringToWikiTranslator.java index db32a289c2..06806bd803 100644 --- a/jspwiki-wysiwyg/src/main/java/org/apache/wiki/htmltowiki/HtmlStringToWikiTranslator.java +++ b/jspwiki-wysiwyg/src/main/java/org/apache/wiki/htmltowiki/HtmlStringToWikiTranslator.java @@ -20,13 +20,18 @@ Licensed to the Apache Software Foundation (ASF) under one import org.apache.wiki.api.core.Context; import org.apache.wiki.api.core.Engine; -import org.codelibs.nekohtml.parsers.SAXParser; +import org.htmlunit.cyberneko.parsers.SAXParser; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.jdom2.input.SAXBuilder; -import org.jdom2.input.sax.XMLReaderSAX2Factory; +import org.jdom2.input.sax.XMLReaderJDOMFactory; import org.jdom2.output.XMLOutputter; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.AttributesImpl; +import org.xml.sax.helpers.XMLFilterImpl; import java.io.IOException; import java.io.StringReader; @@ -38,7 +43,19 @@ Licensed to the Apache Software Foundation (ASF) under one */ public class HtmlStringToWikiTranslator { - private static final String CYBERNEKO_PARSER = SAXParser.class.getName(); + /** Hands JDOM the HTML parser directly, as it would set SAX features on it that the parser does not know about. */ + private static final XMLReaderJDOMFactory HTML_READER_FACTORY = new XMLReaderJDOMFactory() { + @Override + public XMLReader createXMLReader() { + return new AttributeUriFilter( new SAXParser() ); + } + + @Override + public boolean isValidating() { + return false; + } + }; + private final Engine e; /** @@ -97,12 +114,12 @@ public String translate( final String html, final XHtmlToWikiConfig config ) thr * Use NekoHtml to parse HTML like well-formed XHTML * * @param html HTML to parse. - * @return xhtml jdom root element (node "HTML") + * @return xhtml jdom root element (node "html") * @throws JDOMException when errors occur in parsing * @throws IOException when an I/O error prevents a document from being fully parsed */ private Element htmlStringToElement( final String html ) throws JDOMException, IOException { - final SAXBuilder builder = new SAXBuilder( new XMLReaderSAX2Factory( true, CYBERNEKO_PARSER ), null, null ); + final SAXBuilder builder = new SAXBuilder( HTML_READER_FACTORY, null, null ); //builder.setProperty( XMLConstants.ACCESS_EXTERNAL_DTD, "" ); //builder.setProperty( XMLConstants.ACCESS_EXTERNAL_SCHEMA, "" ); final Document doc = builder.build( new StringReader( html ) ); @@ -121,4 +138,27 @@ public static String element2String( final Element element ) { return outputter.outputString( document ); } + /** + * The HTML parser reports {@code null} as the namespace URI of the attributes, where the SAX contract says it is the empty string, + * and JDOM cannot handle that. + */ + private static final class AttributeUriFilter extends XMLFilterImpl { + + AttributeUriFilter( final XMLReader parent ) { + super( parent ); + } + + @Override + public void startElement( final String uri, final String localName, final String qName, final Attributes atts ) throws SAXException { + final AttributesImpl attributes = new AttributesImpl( atts ); + for( int i = 0; i < attributes.getLength(); i++ ) { + if( attributes.getURI( i ) == null ) { + attributes.setURI( i, "" ); + } + } + super.startElement( uri, localName, qName, attributes ); + } + + } + } diff --git a/jspwiki-wysiwyg/src/main/java/org/apache/wiki/htmltowiki/XHtmlElementToWikiTranslator.java b/jspwiki-wysiwyg/src/main/java/org/apache/wiki/htmltowiki/XHtmlElementToWikiTranslator.java index b096cd5d17..dd3105df7f 100644 --- a/jspwiki-wysiwyg/src/main/java/org/apache/wiki/htmltowiki/XHtmlElementToWikiTranslator.java +++ b/jspwiki-wysiwyg/src/main/java/org/apache/wiki/htmltowiki/XHtmlElementToWikiTranslator.java @@ -130,14 +130,14 @@ public void translateText( final Text element ) { } public void translateImage( final Element base ) { - Element child = XmlUtil.getXPathElement( base, "TBODY/TR/TD/*" ); + Element child = XmlUtil.getXPathElement( base, "tbody/tr/td/*" ); if( child == null ) { child = base; } final Element img; final String href; - if( child.getName().equals( "A" ) ) { - img = child.getChild( "IMG" ); + if( child.getName().equals( "a" ) ) { + img = child.getChild( "img" ); href = child.getAttributeValue( "href" ); } else { img = child; @@ -156,7 +156,7 @@ public void translateImage( final Element base ) { putIfNotEmpty( imageAttrs, "height", img.getAttributeValue( "height" ) ); putIfNotEmpty( imageAttrs, "width", img.getAttributeValue( "width" ) ); putIfNotEmpty( imageAttrs, "alt", img.getAttributeValue( "alt" ) ); - putIfNotEmpty( imageAttrs, "caption", emptyToNull( ( Element )XPathFactory.instance().compile( "CAPTION" ).evaluateFirst( base ) ) ); + putIfNotEmpty( imageAttrs, "caption", emptyToNull( ( Element )XPathFactory.instance().compile( "caption" ).evaluateFirst( base ) ) ); putIfNotEmpty( imageAttrs, "link", href ); putIfNotEmpty( imageAttrs, "border", img.getAttributeValue( "border" ) ); putIfNotEmpty( imageAttrs, "style", base.getAttributeValue( "style" ) ); @@ -288,7 +288,7 @@ public void translateChildren( final Element base ) throws JDOMException { void translateA( final Element e ) throws JDOMException { if( config.isNotIgnorableWikiMarkupLink( e ) ) { - if( e.getChild( "IMG" ) != null ) { + if( e.getChild( "img" ) != null ) { translateImage( e ); } else { final String ref = config.trimLink( e.getAttributeValue( "href" ) ); diff --git a/jspwiki-wysiwyg/src/main/java/org/apache/wiki/htmltowiki/syntax/FormDecorator.java b/jspwiki-wysiwyg/src/main/java/org/apache/wiki/htmltowiki/syntax/FormDecorator.java index 25a9331df3..b293747e57 100644 --- a/jspwiki-wysiwyg/src/main/java/org/apache/wiki/htmltowiki/syntax/FormDecorator.java +++ b/jspwiki-wysiwyg/src/main/java/org/apache/wiki/htmltowiki/syntax/FormDecorator.java @@ -46,7 +46,7 @@ protected FormDecorator( final PrintWriter out, final XHtmlElementToWikiTranslat */ public void decorate( final Element e ) throws JDOMException { // remove the hidden input where name="formname" since a new one will be generated again when the xhtml is rendered. - final Element formName = XmlUtil.getXPathElement( e, "INPUT[@name='formname']" ); + final Element formName = XmlUtil.getXPathElement( e, "input[@name='formname']" ); if( formName != null ) { formName.detach(); } diff --git a/pom.xml b/pom.xml index b76bcfa182..04c437768b 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ 2.26.1 9.12.2 5.20.0 - 2.1.3 + 5.6.0 2.0.8 2.1.0 0.5 @@ -424,9 +424,9 @@ - org.codelibs - nekohtml - ${nekohtml.version} + org.htmlunit + neko-htmlunit + ${neko-htmlunit.version} From 4c89c2d1b2e3532b82bc9c4795c1094fe1ee4c88 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Sat, 31 Oct 2026 18:56:43 +0100 Subject: [PATCH 2/4] JSPWIKI-1206 Add the entry of the change to the ChangeLog --- ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog.md b/ChangeLog.md index 6950e04176..994de12b3a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -39,6 +39,7 @@ under the License. * Fix for [JSPWIKI-922](https://issues.apache.org/jira/browse/JSPWIKI-922): the `Content-Disposition` header of an attachment download carried a name with non-ASCII characters as a raw byte, which is not valid UTF-8; such names get an ASCII fallback and a `filename*` parameter with the UTF-8 name now ([PR#571](https://github.com/apache/jspwiki/pull/571)) * Fix for [JSPWIKI-501](https://issues.apache.org/jira/browse/JSPWIKI-501): wildcard and prefix searches that contain an underscore find the pages ([PR#543](https://github.com/apache/jspwiki/pull/543)) +* [JSPWIKI-1206](https://issues.apache.org/jira/browse/JSPWIKI-1206) and [JSPWIKI-1221](https://issues.apache.org/jira/browse/JSPWIKI-1221): NekoHTML is replaced by htmlunit-neko 5.6.0 in the HTML to wiki translator ([PR#548](https://github.com/apache/jspwiki/pull/548)) * Fix for [JSPWIKI-1138](https://issues.apache.org/jira/browse/JSPWIKI-1138): Install.jsp echoed submitted values without escaping them; they are escaped now ([PR#323](https://github.com/apache/jspwiki/pull/323)) * Fix for [JSPWIKI-1164](https://issues.apache.org/jira/browse/JSPWIKI-1164): a page reached from a search result showed `<` as `<` when the query was highlighted ([PR#538](https://github.com/apache/jspwiki/pull/538)) * Fix for [JSPWIKI-919](https://issues.apache.org/jira/browse/JSPWIKI-919): `VersioningFileProvider#pageExists()` ignored the first version of a page that has no history, so a page dropped into the page directory was reported as missing when the page cache is off ([PR#539](https://github.com/apache/jspwiki/pull/539)) From 7e51af7d5f808b80b54aa31b3937c58ef4eb23f3 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Wed, 4 Nov 2026 06:51:09 +0100 Subject: [PATCH 3/4] JSPWIKI-1206 Update htmlunit-neko to 5.7.0 5.7.0 limits the input the parser accepts, which guards against denial of service with crafted HTML, and drops the first line break after
, as
the HTML specification says. The tests of 
 expect one line break less.
---
 LICENSE                                                       | 2 +-
 .../markdown/HtmlStringToMarkdownWikiTranslatorTest.java      | 4 ++--
 .../wiki/htmltowiki/HtmlStringToWikiTranslatorTest.java       | 4 ++--
 pom.xml                                                       | 2 +-
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/LICENSE b/LICENSE
index 4c5da67dad..204f7461bb 100644
--- a/LICENSE
+++ b/LICENSE
@@ -270,7 +270,7 @@ lucene-queries-9.12.2.jar                   LICENSE
 lucene-queryparser-9.12.2.jar               LICENSE
 lucene-sandbox-9.12.2.jar                   LICENSE
 javax.mail-1.6.2.jar                        ./jspwiki-war/src/main/config/doc/LICENSE.cddl
-neko-htmlunit-5.6.0.jar                     LICENSE
+neko-htmlunit-5.7.0.jar                     LICENSE
 org.suigeneris.jrcs.diff-0.4.2              LICENSE
 oro-2.0.8.jar                               LICENSE
 sandler-0.5.jar                             ./jspwiki-war/src/main/config/doc/LICENSE.sandler
diff --git a/jspwiki-markdown/src/test/java/org/apache/wiki/htmltowiki/syntax/markdown/HtmlStringToMarkdownWikiTranslatorTest.java b/jspwiki-markdown/src/test/java/org/apache/wiki/htmltowiki/syntax/markdown/HtmlStringToMarkdownWikiTranslatorTest.java
index edacfd491c..15958d48bc 100644
--- a/jspwiki-markdown/src/test/java/org/apache/wiki/htmltowiki/syntax/markdown/HtmlStringToMarkdownWikiTranslatorTest.java
+++ b/jspwiki-markdown/src/test/java/org/apache/wiki/htmltowiki/syntax/markdown/HtmlStringToMarkdownWikiTranslatorTest.java
@@ -159,9 +159,9 @@ public void testLists() throws Exception {
     public void testPre() throws Exception {
         Assertions.assertEquals( "\n``hallo``\n", html2wiki.translate( "
hallo
" ) ); Assertions.assertEquals( "\n``Hallo\nWelt!\n\n``\n", html2wiki.translate( "
Hallo
Welt!

" ) ); - Assertions.assertEquals( "\n``\n\n\n\nHallo\n\n\n\nWelt!\n\n\n\n``\n", html2wiki.translate( "\n\n\n\n
\n\n\n\nHallo\n\n\n\nWelt!\n\n\n\n
\n\n\n\n" ) ); + Assertions.assertEquals( "\n``\n\n\nHallo\n\n\n\nWelt!\n\n\n\n``\n", html2wiki.translate( "\n\n\n\n
\n\n\n\nHallo\n\n\n\nWelt!\n\n\n\n
\n\n\n\n" ) ); - Assertions.assertEquals( "\n``\n\n* Baltramon \n lasdjfh\n\n``\n", html2wiki.translate( "
\n\n* Baltramon \n  lasdjfh\n\n
" ) ); + Assertions.assertEquals( "\n``\n* Baltramon \n lasdjfh\n\n``\n", html2wiki.translate( "
\n\n* Baltramon \n  lasdjfh\n\n
" ) ); /* * // The style "font-family: courier new" is no longer translated as monospace text, so this test case is no longer needed. Assertions.assertEquals( diff --git a/jspwiki-wysiwyg/src/test/java/org/apache/wiki/htmltowiki/HtmlStringToWikiTranslatorTest.java b/jspwiki-wysiwyg/src/test/java/org/apache/wiki/htmltowiki/HtmlStringToWikiTranslatorTest.java index ede19f9da1..9010eb84c5 100644 --- a/jspwiki-wysiwyg/src/test/java/org/apache/wiki/htmltowiki/HtmlStringToWikiTranslatorTest.java +++ b/jspwiki-wysiwyg/src/test/java/org/apache/wiki/htmltowiki/HtmlStringToWikiTranslatorTest.java @@ -161,10 +161,10 @@ public void testLists() throws Exception { public void testPre() throws Exception { Assertions.assertEquals( "\n{{{hallo}}}\n", html2wiki.translate( "
hallo
" ) ); Assertions.assertEquals( "\n{{{Hallo\nWelt!\n\n}}}\n", html2wiki.translate( "
Hallo
Welt!

" ) ); - Assertions.assertEquals( "\n{{{\n\n\n\nHallo\n\n\n\nWelt!\n\n\n\n}}}\n", + Assertions.assertEquals( "\n{{{\n\n\nHallo\n\n\n\nWelt!\n\n\n\n}}}\n", html2wiki.translate( "\n\n\n\n
\n\n\n\nHallo\n\n\n\nWelt!\n\n\n\n
\n\n\n\n" ) ); - Assertions.assertEquals( "\n{{{\n\n* Baltramon \n lasdjfh\n\n}}}\n", html2wiki.translate( "
\n\n* Baltramon \n  lasdjfh\n\n
" ) ); + Assertions.assertEquals( "\n{{{\n* Baltramon \n lasdjfh\n\n}}}\n", html2wiki.translate( "
\n\n* Baltramon \n  lasdjfh\n\n
" ) ); /* * // The style "font-family: courier new" is no longer translated as monospace text, so this test case is no longer needed. Assertions.assertEquals( diff --git a/pom.xml b/pom.xml index 04c437768b..c79cd961c0 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ 2.26.1 9.12.2 5.20.0 - 5.6.0 + 5.7.0 2.0.8 2.1.0 0.5 From 16aa3705ea3ef35cafb3a6c9e04a5b46b5d58df1 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Wed, 4 Nov 2026 06:51:09 +0100 Subject: [PATCH 4/4] JSPWIKI-1206 Add the entry of the change to the ChangeLog --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 994de12b3a..328395a4c8 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -39,7 +39,7 @@ under the License. * Fix for [JSPWIKI-922](https://issues.apache.org/jira/browse/JSPWIKI-922): the `Content-Disposition` header of an attachment download carried a name with non-ASCII characters as a raw byte, which is not valid UTF-8; such names get an ASCII fallback and a `filename*` parameter with the UTF-8 name now ([PR#571](https://github.com/apache/jspwiki/pull/571)) * Fix for [JSPWIKI-501](https://issues.apache.org/jira/browse/JSPWIKI-501): wildcard and prefix searches that contain an underscore find the pages ([PR#543](https://github.com/apache/jspwiki/pull/543)) -* [JSPWIKI-1206](https://issues.apache.org/jira/browse/JSPWIKI-1206) and [JSPWIKI-1221](https://issues.apache.org/jira/browse/JSPWIKI-1221): NekoHTML is replaced by htmlunit-neko 5.6.0 in the HTML to wiki translator ([PR#548](https://github.com/apache/jspwiki/pull/548)) +* [JSPWIKI-1206](https://issues.apache.org/jira/browse/JSPWIKI-1206) and [JSPWIKI-1221](https://issues.apache.org/jira/browse/JSPWIKI-1221): NekoHTML is replaced by htmlunit-neko 5.7.0 in the HTML to wiki translator ([PR#548](https://github.com/apache/jspwiki/pull/548)) * Fix for [JSPWIKI-1138](https://issues.apache.org/jira/browse/JSPWIKI-1138): Install.jsp echoed submitted values without escaping them; they are escaped now ([PR#323](https://github.com/apache/jspwiki/pull/323)) * Fix for [JSPWIKI-1164](https://issues.apache.org/jira/browse/JSPWIKI-1164): a page reached from a search result showed `<` as `<` when the query was highlighted ([PR#538](https://github.com/apache/jspwiki/pull/538)) * Fix for [JSPWIKI-919](https://issues.apache.org/jira/browse/JSPWIKI-919): `VersioningFileProvider#pageExists()` ignored the first version of a page that has no history, so a page dropped into the page directory was reported as missing when the page cache is off ([PR#539](https://github.com/apache/jspwiki/pull/539))