View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.html.parser.neko;
16  
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.io.StringReader;
20  import java.net.URL;
21  import java.nio.charset.Charset;
22  import java.util.ArrayList;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.concurrent.ConcurrentHashMap;
26  
27  import org.htmlunit.ObjectInstantiationException;
28  import org.htmlunit.Page;
29  import org.htmlunit.SgmlPage;
30  import org.htmlunit.WebAssert;
31  import org.htmlunit.WebClient;
32  import org.htmlunit.WebResponse;
33  import org.htmlunit.cyberneko.HTMLScanner;
34  import org.htmlunit.cyberneko.HTMLTagBalancer;
35  import org.htmlunit.cyberneko.xerces.util.DefaultErrorHandler;
36  import org.htmlunit.cyberneko.xerces.xni.QName;
37  import org.htmlunit.cyberneko.xerces.xni.XNIException;
38  import org.htmlunit.cyberneko.xerces.xni.parser.XMLErrorHandler;
39  import org.htmlunit.cyberneko.xerces.xni.parser.XMLInputSource;
40  import org.htmlunit.cyberneko.xerces.xni.parser.XMLParseException;
41  import org.htmlunit.html.DefaultElementFactory;
42  import org.htmlunit.html.DomNode;
43  import org.htmlunit.html.ElementFactory;
44  import org.htmlunit.html.Html;
45  import org.htmlunit.html.HtmlPage;
46  import org.htmlunit.html.HtmlSvg;
47  import org.htmlunit.html.UnknownElementFactory;
48  import org.htmlunit.html.XHtmlPage;
49  import org.htmlunit.html.parser.HTMLParser;
50  import org.htmlunit.html.parser.HTMLParserListener;
51  import org.htmlunit.svg.SvgElementFactory;
52  import org.htmlunit.svg.SvgForeignObject;
53  import org.htmlunit.util.StringUtils;
54  import org.w3c.dom.Node;
55  import org.xml.sax.SAXException;
56  
57  /**
58   * <p>SAX parser implementation that uses the NekoHTML {@link org.htmlunit.cyberneko.HTMLConfiguration}
59   * to parse HTML into a HtmlUnit-specific DOM (HU-DOM) tree.</p>
60   *
61   * @author Christian Sell
62   * @author David K. Taylor
63   * @author Chris Erskine
64   * @author Ahmed Ashour
65   * @author Marc Guillemot
66   * @author Ethan Glasser-Camp
67   * @author Sudhan Moghe
68   * @author Ronald Brill
69   * @author Frank Danek
70   * @author Carsten Steul
71   */
72  public final class HtmlUnitNekoHtmlParser implements HTMLParser {
73  
74      /**
75       * The SVG factory.
76       */
77      public static final SvgElementFactory SVG_FACTORY = new SvgElementFactory();
78  
79      private static final Map<String, ElementFactory> ELEMENT_FACTORIES = new ConcurrentHashMap<>();
80  
81      static {
82          final DefaultElementFactory defaultElementFactory = new DefaultElementFactory();
83          for (final String tagName : DefaultElementFactory.SUPPORTED_TAGS_) {
84              ELEMENT_FACTORIES.put(tagName, defaultElementFactory);
85          }
86      }
87  
88      /**
89       * {@inheritDoc}
90       */
91      @Override
92      public void parseFragment(final WebClient webClient, final DomNode parent, final DomNode context,
93              final String source, final boolean createdByJavascript)
94          throws SAXException, IOException {
95          final Page page = parent.getPage();
96          if (!(page instanceof HtmlPage htmlPage)) {
97              return;
98          }
99          final URL url = htmlPage.getUrl();
100 
101         final HtmlUnitNekoDOMBuilder domBuilder =
102                 new HtmlUnitNekoDOMBuilder(this, webClient, parent, url, source, createdByJavascript);
103         domBuilder.setFeature("http://cyberneko.org/html/features/balance-tags/document-fragment", true);
104         // the content of an svg element (e.g. svg.innerHTML) is svg content
105         domBuilder.setInsideSvg(isSvgContext(context));
106 
107         final boolean xhtml = page instanceof XHtmlPage;
108         domBuilder.setFeature(HTMLScanner.ALLOW_SELFCLOSING_TAGS, xhtml);
109         domBuilder.setFeature(HTMLScanner.ALLOW_SELFCLOSING_IFRAME, xhtml);
110         domBuilder.setFeature(HTMLScanner.ALLOW_SELFCLOSING_SCRIPT, xhtml);
111 
112         // build fragment context stack
113         DomNode node = context;
114         final List<QName> ancestors = new ArrayList<>();
115         while (node != null && node.getNodeType() != Node.DOCUMENT_NODE) {
116             ancestors.add(0, new QName(null, node.getNodeName(), null, null));
117             node = node.getParentNode();
118         }
119         if (ancestors.isEmpty() || !"html".equals(ancestors.get(0).getLocalpart())) {
120             ancestors.add(new QName(null, "html", null, null));
121             ancestors.add(new QName(null, "body", null, null));
122         }
123         else if (ancestors.size() == 1
124                 || (!"body".equals(ancestors.get(1).getLocalpart())
125                         && !"head".equals(ancestors.get(1).getLocalpart()))) {
126             ancestors.add(new QName(null, "body", null, null));
127         }
128 
129         domBuilder.setProperty(HTMLTagBalancer.FRAGMENT_CONTEXT_STACK, ancestors.toArray(new QName[0]));
130 
131         final XMLInputSource in = new XMLInputSource(null, url.toString(), null, new StringReader(source), null);
132 
133         htmlPage.registerParsingStart();
134         htmlPage.registerSnippetParsingStart();
135         try {
136             domBuilder.parse(in);
137         }
138         finally {
139             htmlPage.registerParsingEnd();
140             htmlPage.registerSnippetParsingEnd();
141         }
142     }
143 
144     /**
145      * {@inheritDoc}
146      */
147     @Override
148     public void parse(final WebClient webClient, final WebResponse webResponse, final HtmlPage page,
149             final boolean xhtml, final boolean createdByJavascript) throws IOException {
150         final URL url = webResponse.getWebRequest().getUrl();
151         final HtmlUnitNekoDOMBuilder domBuilder =
152                 new HtmlUnitNekoDOMBuilder(this, webClient, page, url, null, createdByJavascript);
153 
154         final Charset charset = webResponse.getContentCharset();
155         try {
156             if (!webResponse.wasContentCharsetTentative()) {
157                 // The charset is certain so ignore any others found in the document
158                 domBuilder.setFeature(HTMLScanner.IGNORE_SPECIFIED_CHARSET, true);
159             }
160 
161             // xml content is different
162             if (xhtml) {
163                 domBuilder.setFeature(HTMLScanner.ALLOW_SELFCLOSING_TAGS, true);
164                 domBuilder.setFeature(HTMLScanner.ALLOW_SELFCLOSING_IFRAME, true);
165                 domBuilder.setFeature(HTMLScanner.ALLOW_SELFCLOSING_SCRIPT, true);
166 
167                 domBuilder.setFeature(HTMLScanner.SCRIPT_STRIP_CDATA_DELIMS, true);
168                 domBuilder.setFeature(HTMLScanner.STYLE_STRIP_CDATA_DELIMS, true);
169                 domBuilder.setFeature(HTMLScanner.CDATA_SECTIONS, true);
170                 domBuilder.setFeature(HTMLScanner.CDATA_EARLY_CLOSING, false);
171             }
172 
173             final int bufferSize = webClient.getOptions().getNekoReaderBufferSize();
174             if (bufferSize > 0) {
175                 domBuilder.setProperty(HTMLScanner.READER_BUFFER_SIZE, bufferSize);
176             }
177         }
178         catch (final Exception e) {
179             throw new ObjectInstantiationException("Error setting HTML parser feature", e);
180         }
181 
182         try (InputStream content = webResponse.getContentAsStream()) {
183             final String encoding = charset.name();
184             final XMLInputSource in = new XMLInputSource(null, url.toString(), null, content, encoding);
185 
186             page.registerParsingStart();
187             try {
188                 domBuilder.parse(in);
189             }
190             catch (final XNIException e) {
191                 // extract enclosed exception
192                 final Throwable origin = extractNestedException(e);
193                 throw new RuntimeException("Failed parsing content from " + url, origin);
194             }
195         }
196         finally {
197             page.registerParsingEnd();
198         }
199     }
200 
201     /**
202      * Extract nested exception within an XNIException.
203      *
204      * @param e the original XNIException
205      * @return the cause exception
206      */
207     static Throwable extractNestedException(final Throwable e) {
208         Throwable originalException;
209         Throwable cause = e;
210         do {
211             originalException = cause;
212 
213             if (cause instanceof XNIException) {
214                 cause = cause.getCause();
215             }
216             else {
217                 cause = null;
218             }
219         }
220         while (cause != null);
221 
222         return originalException;
223     }
224 
225     /**
226      * {@inheritDoc}
227      */
228     @Override
229     public ElementFactory getSvgFactory() {
230         return SVG_FACTORY;
231     }
232 
233     /**
234      * {@inheritDoc}
235      */
236     @Override
237     public ElementFactory getFactory(final String tagName) {
238         final ElementFactory result = ELEMENT_FACTORIES.get(tagName);
239 
240         if (result != null) {
241             return result;
242         }
243         return UnknownElementFactory.INSTANCE;
244     }
245 
246     /**
247      * Checks if the content of the given context node is svg content; this is the case
248      * if the context is an svg element (or inside of one) but not inside of a foreignObject.
249      *
250      * @param context the context node of the fragment
251      * @return true if the content of the context is svg content
252      */
253     private static boolean isSvgContext(final DomNode context) {
254         DomNode node = context;
255         while (node != null) {
256             if (node instanceof SvgForeignObject) {
257                 return false;
258             }
259             if (node instanceof HtmlSvg) {
260                 return true;
261             }
262             node = node.getParentNode();
263         }
264         return false;
265     }
266 
267     /**
268      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
269      *
270      * Returns the pre-registered element factory corresponding to the specified tag, or an UnknownElementFactory.
271      * @param page the page
272      * @param namespaceURI the namespace URI
273      * @param qualifiedName the qualified name
274      * @param insideSvg is the node inside an SVG node or not
275      * @param svgSupport true if called from javascript createElementNS
276      * @return the pre-registered element factory corresponding to the specified tag, or an UnknownElementFactory
277      */
278     @Override
279     public ElementFactory getElementFactory(final SgmlPage page, final String namespaceURI,
280             final String qualifiedName, final boolean insideSvg, final boolean svgSupport) {
281         if (insideSvg) {
282             return SVG_FACTORY;
283         }
284 
285         if (namespaceURI == null || namespaceURI.isEmpty()
286             || Html.XHTML_NAMESPACE.equals(namespaceURI)
287             || Html.SVG_NAMESPACE.equals(namespaceURI)
288             || !qualifiedName.contains(":")) {
289 
290             String tagName = qualifiedName;
291             final int index = tagName.indexOf(':');
292             if (index == -1) {
293                 tagName = StringUtils.toRootLowerCase(tagName);
294             }
295             else {
296                 tagName = tagName.substring(index + 1);
297             }
298             final ElementFactory factory;
299             if (svgSupport && !"svg".equals(tagName) && Html.SVG_NAMESPACE.equals(namespaceURI)) {
300                 factory = SVG_FACTORY;
301             }
302             else {
303                 factory = ELEMENT_FACTORIES.get(tagName);
304             }
305 
306             if (factory != null) {
307                 return factory;
308             }
309         }
310         return UnknownElementFactory.INSTANCE;
311     }
312 }
313 
314 /**
315  * Utility to transmit parsing errors to a {@link HTMLParserListener}.
316  */
317 class HtmlUnitNekoHTMLErrorHandler implements XMLErrorHandler {
318     private final HTMLParserListener listener_;
319     private final URL url_;
320     private final String html_;
321 
322     HtmlUnitNekoHTMLErrorHandler(final HTMLParserListener listener, final URL url, final String htmlContent) {
323         WebAssert.notNull("listener", listener);
324         WebAssert.notNull("url", url);
325         listener_ = listener;
326         url_ = url;
327         html_ = htmlContent;
328     }
329 
330     /**
331      * {@inheritDoc}
332      * @see DefaultErrorHandler#error(String,String,XMLParseException)
333      */
334     @Override
335     public void error(final String domain, final String key,
336             final XMLParseException exception) throws XNIException {
337         listener_.error(exception.getMessage(),
338                 url_,
339                 html_,
340                 exception.getLineNumber(),
341                 exception.getColumnNumber(),
342                 key);
343     }
344 
345     /**
346      * {@inheritDoc}
347      * @see DefaultErrorHandler#warning(String,String,XMLParseException)
348      */
349     @Override
350     public void warning(final String domain, final String key,
351             final XMLParseException exception) throws XNIException {
352         listener_.warning(exception.getMessage(),
353                 url_,
354                 html_,
355                 exception.getLineNumber(),
356                 exception.getColumnNumber(),
357                 key);
358     }
359 
360     @Override
361     public void fatalError(final String domain, final String key,
362             final XMLParseException exception) throws XNIException {
363         listener_.error(exception.getMessage(),
364                 url_,
365                 html_,
366                 exception.getLineNumber(),
367                 exception.getColumnNumber(),
368                 key);
369     }
370 }